Fragment View's state is broken by FragmentManager - java

I wanted to retain views through fragment replace operation and noticed a strange bug:
After fragment's View is destroyed through replace operation, it still has a parent, however, the parent doesn't have the view among its children.
private View view;
#Override
public View onCreateView(...) {
if (view == null) {
view = // ... inflate view
} else {
// at this point, view has mParent, but can't be removed
// because the parent does not have it among its children
}
return view;
}
#Override
public void onDestroyView() {
// I could remove the view from its parent here manually,
// however, it will cause fragment's content to disappear before
// the replace animation ends
}
Notice how the view has a reference to its parent through mParent, but the parent has zero mChildrenCount.
This causes fragment transaction to crash with an exception when trying to reuse the view:
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first. I find this behavior to be exceptionally wrong since it leaves the fragment's view in an unusable state.
If someone has faced this issue, please share your workarounds. I have come up with a temporary dirty reflective solution:
#Override
public View onCreateView(...) {
if (view == null) {
view = // ... inflate view
} else {
// clear parent reference (because SDK fails to do it)
Field f = View.class.getDeclaredField("mParent");
f.setAccessible(true);
f.set(view, null);
}
return view;
}

Related

Refreshing a RecyclerView when all items are removed

I have a RecyclerView that shows a list of items.
If there is no item to show, The recyclerview shows one item with a specific view (to tell the user there is no item instead of a white screen).
Within HistoryFragment:
private void initRecyclerView(Boolean isNoResult){
HistoryRecyclerViewAdapter adapter = new HistoryRecyclerViewAdapter(mContext, mRecords, **isNoResult**);
mRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
mRecyclerView.setAdapter(adapter);
}
Within HistoryRecyclerViewAdapter:
#NonNull
#Override
public ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view;
if(**isEmpty**) {
**view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_listitem_prhistory_empty, parent, false);**
} else {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_listitem_prhistory, parent, false);
}
ViewHolder holder = new ViewHolder(view);
return holder;
}
So, it's possible to remove items one by one, if we click on them.
I would like to set isEmpty to true and refresh the RecyclerView when the dataSet is null.
I already know where to call that method but I really don't know how I can do that? (i.e. refresh the RecyclerView with isEmpty = true so I can display the cell that explain to the user that there is no record anymore).
Don't inflate different view-holders, because when the adapter has no items, not a single one of them will ever be inflated. Instead one can wrap the RecyclerView together with a "no data" Fragment into a ViewFlipper, which then can be switched to the Fragment, when the RecyclerView adapter has no items.
Best practice is to use an empty view outside of RecyclerView but in case you like to do what you want:
1.in onCreateViewHolder only inflate one layout which has empty and item views
on item delete check if your array is empty and then add a null item
then in onBindViewHolder check the model if model is Null visible empty view otherwise show item view
summary:
onBind:
model is null : empty View visible
model is not null: item View visible
use interface to refresh the RecyclerView after remove something like this
public interface RefreshRecyclerView {
public void Refresh();
}
then in activity or fragment implement the interface
Fragment implements RefreshRecyclerView
you will have override method like this
#Override
public void Refresh() {
// set adapter again here
}
then pass the interface to adapter like this
RefreshRecyclerView refresh = (RefreshRecyclerView) this;
yourRecycler.setadapter(refresh);
fially when user clicked on adapter use this
refresh. Refresh();

Why do we need to Inflate a layout and attachToRoot in Android?

I was given this code:
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater!!.inflate(R.layout.fragment_wednesday, container, false)
}
and I don't understand why we need to Inflate a layout and write attachToRoot value.
btw, why do we need a viewGroup?
A layout definition is just some XML data but to really show a layout it must be converted to a tree of objects. The inflater does that.
A container (ViewGroup) is necessary to control where (in a larger tree of view objects) the inflated subtree should be placed.
consider this code
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup fragment_container, Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.example_fragment, fragment_container, false);
}
The second parameter fragment_container is a container(framelayout) with id fragment_container that activity uses to add view of fragment in its layout.
No if we read source code of inflate method of LayoutInflater class, we get this( i removed unnecessary shit here to make you understand code better)
// The view that would be returned from this method.
View result = root;
// Temp is the root view that was found in the xml.
final View temp = createViewFromTag(root, name, attrs, false);
Firstly, it creates a temp view from the supplied root.
In case attachToRoot is false, it does this :
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
In case attachToRoot is false, it simply returns the root of the fragment's xml, i.e. the container parameter is just used to get layoutParams for fragment's root view (since it doesn't have a root, so it needs params from somewhere).
In case attachToRoot is true, it does this :
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
It adds the temp view created above to the root view (i.e. container).
The main difference between the "third" parameter attachToRoot being true or false is this.
true : add the child view to parent RIGHT NOW
false: add the child view to parent NOT NOW. Add it later. `
When is that later?
That later is when you use for eg parent.addView(childView)
A common misconception is, if attachToRoot parameter is false then the child view will not be added to parent. WRONG
In both cases child view will be added to parentView. It is just the matter of time.
inflater.inflate(child,parent,false);
parent.addView(child);
is equivalent to
inflater.inflate(child,parent,true);
NOTE !! NOTE !! NOTE !!
You should never pass attachToRoot as true when you are not responsible for adding the child view to parent.
Eg When adding Fragment
public View onCreateView(LayoutInflater inflater,ViewGroup parent,Bundle bundle)
{
super.onCreateView(inflater,parent,bundle);
View v = inflater.inflate(R.layout.image_fragment,parent,false);
return v;
}
now , if you pass third parameter as true you will get IllegalStateException because of the following piece of code.
getSupportFragmentManager()
.beginTransaction()
.add(parent, childFragment)
.commit();
Since you have already added the child fragment in onCreateView() by mistake. Calling add will tell you that child view is already added to parent , hence IllegalStateException. This Exception comes from the following piece of code which can be found while inspecting inflate method in LayoutInflater class
if (child.getParent() != null) {
throw new IllegalStateException("The specified child already has a parent. " +
"You must call removeView() on the child's parent first.");
}
Here you are not responsible for adding childView, FragmentManager is responsible. So always pass false in this case

Use same fragment in ViewPager but fragment will have different layout each time

I want to keep my application thin.
Problem: I would like to reuse my Fragment class code to create 3 different instances in the ViewPager which will have 3 pages. Each Fragment will have a different ImageView or background Drawable. What are best practices regarding this? I noticed that using factory methods like here seem to be good, any other alternatives?
I have one Fragment which has the following methods:
Fragment.java
public static Fragment newInstance(Context context) {
FragmentTutorial f = new FragmentTutorial();
Bundle args = new Bundle();
return f;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment, null);
return root;
}
I have a ViewPagerAdapter class which has the following methods:
ViewPagerAdapter.java
public ViewPagerAdapter(Context context, FragmentManager fm) {
super(fm);
mContext = context;
}
#Override
public Fragment getItem(int position) {
return new FragmentTutorial().newInstance(mContext);
}
#Override
public int getCount() {
return totalPage;
}
What I've found is the "best" way to do it (in my opinion, of course) is to do the following:
Have the fragment contain methods to set the customizable data (background, text, etc)
Note: Be careful of trying to load the data in when first creating the fragment. You may be able to set the data before onCreateView() even runs, or at other times it may run after onCreateView(). I personally use a boolean to check if the data has been set. Inside onCreateView() [or onActivityCreated()], I check if the data has been set already. If it has, load in the data. Alternatively, while setting the data, I check if the views have been created/cached already. This is done by simply having variables to cache the data, say private ImageView mBackgroundView. If the view is not null, then I safely set the data on the views.
The above is also an alternative to using newInstance, although both methods work pretty well. However, for more flexibility, I only use newInstance if a) the data is already known before the fragment has to be inserted and b) the data doesn't need to change according to input from elsewhere much.
Let the ViewPager handle all the data
Pass in all the data - a list of ImageViews, a array of Strings, define where all the data is in Resources, etc - at the very beginning [say, in the constructor]
Have the ViewPager create an ArrayList of the fragments- set up each fragment as early as possible (say when first getting all the data) and add it to the list
Let getCount() just use the size of the list
Let getItem() just get the item in the list at the position
Note: If you have any dynamic data, set it up in the getItem() method. Furthermore, you can always add more data+fragments during runtime as well [just notify the adapter that the dataset has been changed]
Essentially, the fragment is like a simple servant- it does simply the least work necessary. If it doesn't have to handle choosing the data, all the better. It'll thus be far more flexible. Just give methods to set the data/views appropriately on the fragment. Now, the ArrayAdapter can do all the grimy hard work with managing the data and giving it to the appropriate fragment. Take advantage of that.
Now, note that this is assuming you want to use a single layout but want to change different aspects of that layout (texts, background, etc). If you want to make a master fragment class that can use any sort of defined layout, you can but note that it decreases the runtime flexibility (how can you change the text or background to something you get from the internet? You simply can't if you only can define and choose from pre-set layouts).
Either way, the ArrayAdapter should take care of all the different data while the fragment simply does as it's designed to do, in a more flexible manner preferably.
Edit:
Here is the project where I most recently implemented this sort of pattern. Note that it has far more to it, so I'll replace it with some not-so-pseudo pseudo-code in the morning/afternoon.
ViewPager [a bit sloppy with all the different things I was trying to do, including extending from a FragmentStatePagerAdapter without actually using any of the specific features of a StatePagerAdapter. In other words, I still need to work on the lifecycle implementations everywhere]
Fragment [Also may be a bit sloppy but shows the pattern still]
The object (actually another fragment) that uses the ViewPager [it's actually a "VerticalViewpager" from a library, but other than the animations and direction to change the current fragment, it's exactly the same- particularly code-wise]
Edit2:
Here is a more (if overly) simplified example of the pattern described above.
Disclaimer: The following code has absolutely no lifecycle management implementations and is older code that has been untouched since around August '14
Fragment simply allows the user of the fragment to set the background color and the text of the single TextView
Link to BaseFragment
Link to layout file
The adapter creates three instances of the fragment and sets the background color and text of each. Each fragment's text, color, and total fragments is hard coded.
Link to Activity+adapter
Link to layout file
Now, here are the exact relevant portions of the code:
BaseFragment
// Note: Found out later can extend normal Fragments but must use v13 adapter
public class BaseFragment extends android.support.v4.app.Fragment {
FrameLayout mMainLayout; // The parent layout
int mNewColor = 0; // The new bg color, set from activity
String mNewText = ""; // The new text, set from activity
TextView mMainText; // The only textview in this fragment
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate the fragment's layout
View view = inflater.inflate(R.layout.fragment_base,container,false);
// Save the textview for further editing
mMainText = (TextView) view.findViewById(R.id.textView);
// Save the framelayout to change background color later
mMainLayout = (FrameLayout) view.findViewById(R.id.mainLayout);
return view;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// If there is new text or color assigned, set em
if(mNewText != ""){
mMainText.setText(mNewText);
}
if(mNewColor != 0){
mMainLayout.setBackgroundColor(mNewColor);
}
}
#Override
public void onStart(){
super.onStart();
}
// Simply indicate to change the text of the fragment
public void changeText(String newText){
mNewText=newText;
}
// Simply indicate to change the background color of the fragment
public void changeBG(int color) {
// If no color was passed, then set background to white
if(color == 0)
{
mNewColor=getResources().getColor(R.color.white);
}
// else set the color to what was passed in
else{
mNewColor=color;
}
}
}
MyAdapter
class MyAdapter extends FragmentPagerAdapter{
// Three simple fragments
BaseFragment fragA;
BaseFragment fragB;
BaseFragment fragC;
public MyAdapter(FragmentManager fm) {
super(fm);
}
public void setFragments(Context c){
// Set up the simple base fragments
fragA = new BaseFragment();
fragB = new BaseFragment();
fragC = new BaseFragment();
Resources res = c.getResources();
fragA.changeText("This is Fragment A!");
fragB.changeText("This is Fragment B!");
fragC.changeText("This is Fragment C!");
fragA.changeBG(res.getColor(R.color.dev_blue));
fragB.changeBG(res.getColor(R.color.dev_green));
fragC.changeBG(res.getColor(R.color.dev_orange));
}
#Override
public Fragment getItem(int position) {
// TODO: Make this more efficient, use a list or such, also comment more
Fragment frag = null;
if(position == 0){
frag = fragA;
}
else if(position == 1){
frag = fragB;
}
else if(position == 2){
frag = fragC;
}
return frag;
}
#Override
public int getCount() {
return 3;
}
}
You need to pass some sort of id along with newInstance() while creating instance. And according to that id you can use if..else to choose layout file.
See my reference code below:
int id;
public static Fragment newInstance(Context context, int id) {
FragmentTutorial f = new FragmentTutorial();
Bundle args = new Bundle();
this.id = id;
return f;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if(id == 1)
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment1, null);
else
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment2, null);
return root;
}
Can't you just introduce fields to the Fragment class to account for the variances in background, etc. and add them to its constructor? Then in getItem instantiate the Fragment class with different values depending on the value of position.

Specified child already has parent

I'm currently passing data between two dialogfragments. Whenever the dialog fragment 2 had been done. It will pass the data from to dialog fragment 1. Here the method for dialog fragment 1, it implements an interface that created in dialog fragment 2.
Dialog Fragment 1
#Override
public void updateContributor(ArrayList<View> imageView) {
System.out.println("Contributors received from contributer's fragment: " + imageView);
for ( View child : imageView)
{
if (child instanceof ImageView) {
ImageView childImageView = (ImageView) child;
ll_contributor_list.addView(childImageView);
}
}
}
Dialog Fragment 2
MusicRecorderFragment fragment = (MusicRecorderFragment) getFragmentManager().findFragmentByTag("record_fragment");
ArrayList<View> views = getAllChildren(ll);
/*setArguments(args);*/
fragment.updateContributor(views);
getDialog().dismiss();
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
You are getting this exception because of your views are already added in some layout and you are trying to add it again to the contributor list which I guess it's a layout. (ll_contributor_list.addView(childImageView);)
Try to just collect the data and recreate your view on the other fragment instead of trying to add an already added view to another layout.

What is "convertView" parameter in ArrayAdapter getView() method

Can someone tell me what the convertView parameter is used for in the getView() method of the Adapter class?
Here is a sample code take from here:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.row, null);
}
Order o = items.get(position);
if (o != null) {
TextView tt = (TextView) v.findViewById(R.id.toptext);
TextView bt = (TextView) v.findViewById(R.id.bottomtext);
if (tt != null) {
tt.setText("Name: "+o.getOrderName()); }
if(bt != null){
bt.setText("Status: "+ o.getOrderStatus());
}
}
return v;
}
What should we pass via convertView?
What I've found, take from here:
Get a View that displays the data at the specified position in the
data set. You can either create a View manually or inflate it from an
XML layout file. When the View is inflated, the parent View (GridView,
ListView...) will apply default layout parameters unless you use
inflate(int, android.view.ViewGroup, boolean) to specify a root view
and to prevent attachment to the root.
Parameters
position -- The position of the item within the adapter's data set of the item whose view we want.
convertView -- The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before
using. If it is not possible to convert this view to display the
correct data, this method can create a new view.
parent -- The parent that this view will eventually be attached to Returns
returns -- A View corresponding to the data at the specified position.
You shouldn't be calling that method by yourself.
Android's ListView uses an Adapter to fill itself with Views. When the ListView is shown, it starts calling getView() to populate itself. When the user scrolls a new view should be created, so for performance the ListView sends the Adapter an old view that it's not used any more in the convertView param.

Categories