Wednesday, September 13, 2017

Fragments everywhere !!!

Each developer sometimes wants to create something "innovative" and stupid just for the sake of it. And it came to me - why not to put fragments directly into views (and in result in popup windows and dialogs) avoiding activity.

Actually I had requirement on my project that was impossible to finish without this "genius" idea because of some ugly legacy code written by other developer (sure it was not me, my code is perfect :-D).

After digging support library for some time and implementing my own FragmentManager and FragmentTransaction I noticed one important detail - there is no need to reinvent a weal, it was already invented by Android's team. All I had to do is to look at FragmentActivity and find the FragmentController (silly me, I had to look there from the beginning). One interesting thing is that FragmentController is a public class which [in theory] means that we can safely use it.

Ok, so no more talk. Here is my implementation of FragmentManagerView:

    public class FragmentManagerView extends FrameLayout {

        private Activity mActivity;
        private FragmentController mFragments;
        private Handler mHandler = new Handler(Looper.getMainLooper());

        public FragmentManagerView(Context context) {
            this(context, null);
        }

        public FragmentManagerView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);

            while (context != null) {
                if (context instanceof Activity) {
                    mActivity = (Activity) context;
                    break;
                }
                if (context instanceof ContextWrapper) {
                    context = ((ContextWrapper) context).getBaseContext();
                } else {
                    break;
                }
            }

            setId(getFragmentRootId());

            mFragments = FragmentController.createController(new HostCallbacks());
            mFragments.attachHost(null);
        }

        @Override
        protected void onAttachedToWindow() {
            super.onAttachedToWindow();

            mFragments.dispatchCreate();
            mFragments.dispatchActivityCreated();
            mFragments.onCreateView(this, "", getContext(), null);
            mFragments.dispatchStart();
            mFragments.dispatchResume();
            mFragments.execPendingActions();
        }

        @Override
        protected void onDetachedFromWindow() {
            mFragments.execPendingActions();
            mFragments.dispatchPause();
            mFragments.dispatchStop();
            mFragments.dispatchReallyStop();
            mFragments.dispatchDestroyView();
            mFragments.dispatchDestroy();

            super.onDetachedFromWindow();
        }

        public int getFragmentRootId() {
            return android.R.id.content;
        }

        public FragmentManager getFragmentManager() {
            return mFragments.getSupportFragmentManager();
        }

        private class HostCallbacks extends FragmentHostCallback<FragmentManagerView> {
            HostCallbacks() {
                super(mActivity, mActivity, mHandler, 0);
            }

            @Override
            public boolean onHasView() {
                return true;
            }

            @Override
            public View onFindViewById(int id) {
                return findViewById(id);
            }

            @Override
            public FragmentManagerView onGetHost() {
                return FragmentManagerView.this;
            }

            @Override
            public LayoutInflater onGetLayoutInflater() {
                return mActivity.getLayoutInflater().cloneInContext(mActivity);
            }
        }
    }

And this is how you can use it:

    public class MyInnovativeView extends FragmentManagerView {
        @Override
        protected void onAttachedToWindow() {
            super.onAttachedToWindow();

            getFragmentManager().beginTransaction()
                    .replace(getFragmentRootId(), MyFragment.newInstance())
                    .commit();
        }
    }

    public class MyFragment extends Fragment {
        // ...
    }

It works almost flawlessly. This solution even works for popup windows and dialogs. But there are still few drawbacks (actually those are all downsides that I found so far):

  • getActivity() returns null in all fragments

    This one is what I actually don't understand. FragmentHostCallback has constructor to provide activity (which then will be returned via Fragment.getActivity()) but it has package access. Same for FragmentHostCallback.getActivty() method. Because of this you can't legitimately use it.

    But where is a will, there is a way!
    We only need to place our FragmentManagerView in android.support.v4.app and by magic of great Java we'll get required access.
  • no state saving

    It can be easily implemented using FragmentController.saveAllState() / View.onSaveInstanceState() and View.onRestoreInstanceState() / FragmentController.restoreAllState()


And now few final words - DON'T USE IT

  • Yes, I used it on my project.
  • Yes, all required classed are available and public.
But

  • No, it does mean that you can safely use it and it will work for all cases.
  • No, I don't know if this API was designed for such cases.
  • No, there are no quranties that nothing will be changed in the future.

And most importantly

  • Don't code in ways that force you to use solutions like this.


Have a nive coding ;)