Best practice for nested fragments in Android 4.0, 4.1 (<4.2) without using the support library
Asked Answered
B

5

115

I'm writing an app for 4.0 and 4.1 tablets, for which I do not want to use the support libraries (if not needed) but the 4.x api only therefore.

So my target platform is very well defined as: >= 4.0 and <= 4.1

The app has a multi-pane layout (two fragments, one small on the left, one content fragment on the right) and an action bar with tabs.

Similar to this:

enter image description here

Clicking a tab on the action bar changes the 'outer' fragment, and the inner fragment then is a fragment with two nested fragments (1. small left list fragment, 2. wide content fragment).

I am now wondering what's the best practice to replace fragments and especially nested fragments. The ViewPager is part of the support library, there's no native 4.x alternative for this class. Appear to be 'deprecated' in my sense. - http://developer.android.com/reference/android/support/v4/view/ViewPager.html

Then I read the release notes for Android 4.2, regarding ChildFragmentManager, which would be a good fit, but I am targeting 4.0 and 4.1, so this can't be used either.

ChildFragmentManager is only available in 4.2

Unfortunately, there are hardly any good examples out there that show best practices for fragments usages without the support library, even in the entire Android developer guides; and especially nothing regarding nested fragments.

So I am wondering: is it simply not possible to write 4.1 apps with nested fragments without using the support library and everything that comes with it? (need to use FragmentActivity instead of Fragment, etc.?) Or what would be the best practice?


The problem that I am currently having in the development is exactly this statement:

The Android Support Library also now supports nested fragments, so you can implement nested fragment designs on Android 1.6 and higher.

Note: You cannot inflate a layout into a fragment when that layout includes a <fragment>. Nested fragments are only supported when added to a fragment dynamically.

Because I put define the nested fragments in XML, which apparently causes an error like:

Caused by: java.lang.IllegalArgumentException: Binary XML file line #15: Duplicate id 0x7f090009, tag frgCustomerList, or parent id 0x7f090008 with another fragment for de.xyz.is.android.fragment.CustomerListFragment_

At the moment, I conclude for myself: even on 4.1, when I don't even want to target the 2.x platform, nested fragments as shown in the screenshot are not possible without the support library.

(This might actually be more of a wiki entry than a question, but maybe somebody else has managed it before).

Update:

A helpful answer is at: Fragment Inside Fragment

Berlauda answered 25/3, 2013 at 6:29 Comment(9)
You have three options: 1. Target only 4.2 with native nested fragments. 2. Target 4.x with nested fragments from the support library 3. Don't use nested fragments for any other platform target scenarios. This should answer your question.Also, you can't use nested fragment embedded in the xml layout, all of them must be added in code. there are hardly any good examples out there that show best practices for fragments usages without the support library - the support fragment framework replicates the native one so any example should work either way.Hupp
@Luksprog Thanks for your comments. I prefer your solution 2, and framents work well in the support library, but Tabs in the ActionBar do not - afaik, I would need to use ActionBarSherlock, but the tabs would not be integrated in the ActionBar then but only beneath (which isn't necessary for 4.x). And ActionBar.TabListener only support Fragments from android.app.Fragment, not from the support library.Berlauda
Unfortunately there isn't any way to bind the support library's fragments to the native APIs that use fragments so yes you'll need to use ActionBarSherlock. I would need to use ActionBarSherlock, but the tabs would not be integrated in the ActionBar then but only beneath (which isn't necessary for 4.x). - are you sure about that? From what I've seen using the library fully replicates the sdk apis including those tabs.Hupp
@Luksprog Actually, I'm not sure and I am wondering how the native Contacts app on my Samsung Galaxy tab is doing it; I just haven't seen the tabs embedded in the ActionBar in any of the ABS samples anywhere; I will give it a try or find some way to do it; but you are right, I definitely need to use the ABS, since developer.android.com/tools/extras/support-library.html it says: "The ActionBar is not supported by the library. However, when creating your Options Menu, you can declare which items should be added to the Action Bar when it's available (on Android 3.0 or later)"- not useful.Berlauda
I'm not familiar with the Contacts app on the Galaxy tab but keep in mind that you could always face a custom implementation of the ActionBar(built in house by Samsung). Have a closer look at ActionBarSherlock, it has the tabs in the ActionBar if there is room.Hupp
@Luksprog I believe you've already supplied the only answer there is to give, would you be so kind to put it in as a proper answer.Tuft
I don't think it's an answer. It was a very specific question and I'm sure Mathias actually knew the alternatives. What I'm wondering is why this got so many upvotes?! I mean really. So you read about a new feature that is 4.2 only, google provides a way to use the functionality in previous version, and yet, we have a question asking how to use 4.2 functionality in previous versions without having to use the tools that make that happen?Belk
@Pork My main reason for the question is: are there any workarounds for nested fragments without having to use the support library and all it's other view elements. Meaning, if I switch to the support library, I would be using FragmentActivity instead of Fragment. But I do want to use Fragment, all I want is a replacement for Nested Fragments, but not all of the v4 components. I.e. via other open source libraries, etc. out there. For example above screenshot runs on 4.0, and I am wondering if they are using the ABS, SupportLib, or anything else.Berlauda
ABS is built on top of th support library... If you are worried about bloat, that's what proguard is for.Belk
W
61

Limitations

So nesting fragments inside another fragment is not possible with xml regardless of which version of FragmentManager you use.

So you have to add fragments via code, this might seem like a problem, but in the long run makes your layouts superflexible.

So nesting without using getChildFragmentManger? The essence behind childFragmentManager is that it defers loading until the previous fragment transaction has finished. And of course it was only naturally supported in 4.2 or the support library.

Nesting without ChildManager - Solution

Solution, Sure! I have been doing this for a long time now, (since the ViewPager was announced).

See below; This is a Fragment that defers loading, so Fragments can be loaded inside of it.

Its pretty simple, the Handler is a really really handy class, effectively the handler waits for a space to execute on the main thread after the current fragment transaction has finished committing (as fragments interfere with the UI they run on the main thread).

// Remember this is an example, you will need to modify to work with your code
private final Handler handler = new Handler();
private Runnable runPager;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    return inflater.inflate(R.layout.frag_layout, container, false);
}

@Override
public void onActivityCreated(Bundle savedInstanceState)
{
    super.onActivityCreated(savedInstanceState);
    runPager = new Runnable() {

        @Override
        public void run()
        {
          getFragmentManager().beginTransaction().addFragment(R.id.frag_container, MyFragment.newInstance()).commit();
        }
    };
    handler.post(runPager);
}

/**
 * @see android.support.v4.app.Fragment#onPause()
 */
@Override
public void onPause()
{
    super.onPause();
    handler.removeCallbacks(runPager);
}

I wouldn't consider it 'best practice', but I have live apps using this hack and I am yet to have any issues with it.

I also use this method for embedding view pagers - https://gist.github.com/chrisjenx/3405429

Westleigh answered 16/6, 2013 at 10:21 Comment(3)
How do you handle layout nested fragments?Wince
The only way I could see this working is by using a CustomLayoutInflater, as you come across the fragment element, you would override the super implementation and try to parse/inflate it yourself. But that will be ALOT of effort, Well out of scope of a StackOverflow question.Westleigh
Hi, can anyone help me in this issue?? I am really stuck.. #32240638Redaredact
M
2

The best way to do this in pre-API 17 is to not do it at all. Trying to implement this behavior is going to cause issues. However that is not to say that it cannot be faked convincingly using the current API 14. What I did was the following:

1 - look at communication between fragments http://developer.android.com/training/basics/fragments/communicating.html

2 - move your layout xml FrameLayout from your existing Fragment to the Activity layout and hide it by giving a height of 0:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent">
<FrameLayout android:id="@+id/content"
          android:layout_width="300dp"
          android:layout_height="match_parent" />


<FrameLayout android:id="@+id/lstResults"
             android:layout_width="300dp"
             android:layout_height="0dp"
             android:layout_below="@+id/content"
             tools:layout="@layout/treeview_list_content"/>


<FrameLayout android:id="@+id/anomalies_fragment"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
        android:layout_toRightOf="@+id/content" />

3 - Implement the interface in the parent Fragment

    OnListener mCallback;

// Container Activity must implement this interface
public interface OnListener 
{
    public void onDoSomethingToInitChildFrame(/*parameters*/);
    public void showResults();
    public void hideResults();
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);

    // This makes sure that the container activity has implemented
    // the callback interface. If not, it throws an exception
    try {
        mCallback = (OnFilterAppliedListener) activity;
    } catch (ClassCastException e) {
        throw new ClassCastException(activity.toString()
                + " must implement OnListener");
    }
}

@Override
public void onActivityCreated(Bundle savedInstanceState) 
{
    super.onActivityCreated(savedInstanceState);

    mCallback.showResults();
}

@Override
public void onPause()
{
    super.onPause();

    mCallback.hideResults();
}

public void onClickButton(View view)
{
    // do click action here

    mCallback.onDoSomethingToInitChildFrame(/*parameters*/);
}

4 - Implement the interface in the parent Activity

public class YourActivity extends Activity implements yourParentFragment.OnListener {

public void onDoSomethingToInitChildFrame(/*parameters*/)
{
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment childFragment = getFragmentManager().findFragmentByTag("Results");
    if(childFragment == null)
    {
        childFragment = new yourChildFragment(/*parameters*/);
        ft.add(R.id.lstResults, childFragment, "Results");
    }
    else
    {
        ft.detach(childFragment);

        ((yourChildFragment)childFragment).ResetContent(/*parameters*/);

        ft.attach(childFragment);
    }
    ft.commit();

    showResultsPane();
}

public void showResults()
{
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment childFragment = getFragmentManager().findFragmentByTag("Results");
    if(childFragment != null)
        ft.attach(childFragment);
    ft.commit();

    showResultsPane();
}

public void showResultsPane()
{
    //resize the elements to show the results pane
    findViewById(R.id.content).getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
    findViewById(R.id.lstResults).getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
}

public void hideResults()
{
    //resize the elements to hide the results pane
    findViewById(R.id.content).getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
    findViewById(R.id.lstResults).getLayoutParams().height = 0;

    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment childFragment = getFragmentManager().findFragmentByTag("Results");
    if(childFragment != null)
        ft.detach(childFragment);
    ft.commit();
}

}

5 - Enjoy, with this method you get the same fluid functionality as with the getChildFragmentManager() function in a pre-API 17 envoronment. As you may have noticed the child fragment is no longer really a child of the parent fragment but now a child of the activity, this really cannot be avoided.

Mews answered 4/9, 2014 at 15:13 Comment(0)
P
1

I had to deal with this exact issue due to a combination of NavigationDrawer, TabHost, and ViewPager which had complications with usage of the support library because of TabHost. And then I also had to support min API of JellyBean 4.1, so using nested fragments with getChildFragmentManager was not an option.

So my problem can be distilled to...

TabHost (for top level)
+ ViewPager (for just one of the top level tabbed fragments)
= need for Nested Fragments (which JellyBean 4.1 won't support)

My solution was to create the illusion of nested fragments without actually nesting fragments. I did this by having the main activity use TabHost AND ViewPager to manage two sibling Views whose visibility is managed by toggling layout_weight between 0 and 1.

//Hide the fragment used by TabHost by setting height and weight to 0
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 0);
mTabHostedView.setLayoutParams(lp);
//Show the fragment used by ViewPager by setting height to 0 but weight to 1
lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1);
mPagedView.setLayoutParams(lp);

This effectively allowed my fake "Nested Fragment" to operate as an independent view as long as I manually managed the relevant layout weights.

Here's my activity_main.xml:

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.ringofblades.stackoverflow.app.MainActivity">

    <TabHost
        android:id="@android:id/tabhost"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <FrameLayout android:id="@android:id/tabcontent"
                android:background="@drawable/background_image"
                android:layout_width="match_parent"
                android:layout_weight="0.5"
                android:layout_height="0dp"/>
            <android.support.v4.view.ViewPager
                xmlns:tools="http://schemas.android.com/tools"
                android:id="@+id/pager"
                android:background="@drawable/background_image"
                android:layout_width="match_parent"
                android:layout_weight="0.5"
                android:layout_height="0dp"
                tools:context="com.ringofblades.stackoverflow.app.MainActivity">
                <FrameLayout
                    android:id="@+id/container"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" />
            </android.support.v4.view.ViewPager>
            <TabWidget android:id="@android:id/tabs"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </LinearLayout>
    </TabHost>

    <fragment android:id="@+id/navigation_drawer"
        android:layout_width="@dimen/navigation_drawer_width"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:name="com.ringofblades.stackoverflow.app.NavigationDrawerFragment"
        tools:layout="@layout/fragment_navigation_drawer" />
</android.support.v4.widget.DrawerLayout>

Note that "@+id/pager" and "@+id/container" are siblings with 'android:layout_weight="0.5"' and 'android:layout_height="0dp"'. This is so that I can see it in the previewer for any screen size. Their weights will be manipulated in code during runtime, anyway.

Porfirioporgy answered 3/4, 2014 at 9:51 Comment(7)
Hi, I'm curious why you chose to use TabHost instead of ActionBar with Tabs? I myself switched from TabHost to just ActionBar, and my code became cleaner and more compact...Loehr
As far as i remember, one drawback of using tabs in the ActionBar is that it decides automatically to show them as a spinner drop-down menu (in case of a small screen) and that was not good for me. But i'm not 100% sure.Chuddar
@Igor, I read somewhere here on SO that using ActionBar tabs with a Navigation Drawer is not good because it will automatically place the tabs over the view of your drawer. Sorry, I don't have the link to back this up.Thebaid
@NoniA. Regardless, TabHost is gone. Use material design now with Toolbar!Loehr
@Igor, what is "material design with Toolbar"?Thebaid
@NoniA. blog.xamarin.com/android-tips-hello-toolbar-goodbye-action-bar Do you follow Android's development at all?Loehr
Wow, thanks for the link @Igor! I will check this out for sure. I am a beginner still, so have a million other things to learn with Android, but this one seems like a gem! Thanks again.Thebaid
M
1

Building on @Chris.Jenkins answer, this is the solution that has been working well for me, for removing fragment(s) during life cycle events (which have a tendency to throw IllegalStateExceptions). This uses a combination of the Handler approach, and an Activity.isFinishing() check (otherwise it will throw an error for "Can not perform this action after onSaveInstanceState).

import android.app.Activity;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;

public abstract class BaseFragment extends Fragment {
    private final Handler handler = new Handler();

    /**
     * Removes the {@link Fragment} using {@link #getFragmentManager()}, wrapped in a {@link Handler} to
     * compensate for illegal states.
     *
     * @param fragment The {@link Fragment} to schedule for removal.
     */
    protected void removeFragment(@Nullable final Fragment fragment) {
        if (fragment == null) return;

        final Activity activity = getActivity();
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (activity != null && !activity.isFinishing()) {
                    getFragmentManager().beginTransaction()
                            .remove(fragment)
                            .commitAllowingStateLoss();
                }
            }
        });
    }

    /**
     * Removes each {@link Fragment} using {@link #getFragmentManager()}, wrapped in a {@link Handler} to
     * compensate for illegal states.
     *
     * @param fragments The {@link Fragment}s to schedule for removal.
     */
    protected void removeFragments(final Fragment... fragments) {
        final FragmentManager fragmentManager = getFragmentManager();
        final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

        for (Fragment fragment : fragments) {
            if (fragment != null) {
                fragmentTransaction.remove(fragment);
            }
        }

        final Activity activity = getActivity();
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (activity != null && !activity.isFinishing()) {
                    fragmentTransaction.commitAllowingStateLoss();
                }
            }
        });
    }
}

Usage:

class MyFragment extends Fragment {
    @Override
    public void onDestroyView() {
        removeFragments(mFragment1, mFragment2, mFragment3);
        super.onDestroyView();
    }
}
Machellemachete answered 7/8, 2015 at 21:39 Comment(0)
S
1

Although the OP may have special circumstances that prevent him from using the Support Library, most people should use it. The Android documentation recommends it, and it will make your app available to the widest audience possible.

In my fuller answer here I made an example demonstrating how to use nested fragments with the support library.

enter image description here

Simplistic answered 13/9, 2016 at 10:36 Comment(3)
I was the OP. The reason to not use the support lib was because it was a company internal app, where the used hardware was clearly defined as >=4.0 and <=4.1. There was no need to reach a wide audience, it was internal staff and no intention to use the app outside the company. The support lib's only reason is to be backward compatible - but one would expect that everything you can do with the support library, you should be able to achieve "natively" without it. Why would a "native" higher version have less features than a support library which only purpose is just to be downward-compatible.Berlauda
Nevertheless of course you could use the support library and I could've too. Just didn't understand why Google offers features ONLY in the support library but not outside, or why they'd even call it support library and not make it the overall standard then, if it's best practice anyway. Here is a good article about the support library: martiancraft.com/blog/2015/06/android-support-libraryBerlauda
@Mathias, nice article.Simplistic

© 2022 - 2024 — McMap. All rights reserved.