неделя, 30 ноември 2014 г.

Decorating Android views

In this post I will tell you of my struggle to decorate Android views. A battle that lasted for more than two weeks and which, I dare to say, I was finally able to win! I am really proud of it - now I can add custom behavior to my views, while retaining the reusability, code separation, readability and other beautiful properties :)

When did it started? - some while ago when I realised I was faced with a messy code that was adding few features over ordinary ViewPager. I had inheritance, some methods were made static so that they could be called from inadequate places and most code was centralised in one ubiquitous master-of-all-gods class. However, for me it was obvious that we just wanted to add few decorations to the standard ViewPager exactly as promoted by the Decorator design pattern defined in the book of the Gang of four. I, thought, this should be easy. And, furthermore, I wanted to apply this to ViewPager, but from the very beginning it was obvious to me that this would be only my POC - the same strategy should be applicable to all other Android views I feel like decorating.

I am not going to give you the correct solution straight away, rather I will guide you through all my failures until we get there. I will do that as I believe there are some wisdoms to be learned on the long way. Still, if you do not feel like reading too much, feel free to browse down to "The working solution".

Where do we start at?

I have created a special project in Google Code to more easily demonstrate what I am speaking about. It is accessible here - a git repository in which  I will be marking with tags the different stages of interest. The first such is the "base_version" tag - in it we have very simple app showing few pages of text and extended ViewPager that notifies its users when they get out of the pager boundries (try to navigate before the first item or after the last). Note the NotifyOutOfBoundsViewPager, taken from a StackOverflow post.:

public class NotifyOutOfBoundsViewPager extends ViewPager {
    public interface OnSwipeOutListener {
        public void onSwipeOutAtStart();

        public void onSwipeOutAtEnd();
    }

    private float mStartDragX;
    private OnSwipeOutListener onSwipeOutListener;

    public NotifyOutOfBoundsViewPager(Context context) {
        super(context);
    }

    public NotifyOutOfBoundsViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mStartDragX = x;
            break;
        case MotionEvent.ACTION_MOVE:
            if (mStartDragX < x && getCurrentItem() == 0) {
                onSwipeOutListener.onSwipeOutAtStart();
            } else if (mStartDragX > x && getCurrentItem() == getAdapter().getCount() - 1) {
                onSwipeOutListener.onSwipeOutAtEnd();
            }
            break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    public void setOnSwipeOutListener(OnSwipeOutListener listener) {
        this.onSwipeOutListener = listener;
    }
}
This is cool, but now I wanted to add also a timer that will not allow me to read too long this short text. Basically I want a count down that will take some action when the time expires. I really could have added this logic with yet one more extension of the `NotifyOutOfBoundsViewPager` but now it gets even more weird - it turns out i can not get timer in another situation without getting the other extenstion - the notification for out of bounds. I really am felling that we should try to add these two: the notification and the timer as decorations to the view pager so that we can reuse them in other situations too.

Defining the decorator

The first step was obvious to me. I had to define an extension of ViewPager that would support one more parameter - the embedded decoration. Here is how it goes:
public class DecoratedViewPager extends ViewPager {
    private ViewPager viewPager;

    public DecoratedViewPager(Context context, ViewPager viewPager) {
        super(context);
        this.viewPager = viewPager;
    }

    public DecoratedViewPager(Context context, AttributeSet attrs, ViewPager viewPager) {
        super(context, attrs);
        this.viewPager = viewPager;
    }

    @Override
    public void addFocusables(ArrayList arg0, int arg1, int arg2) {
        viewPager.addFocusables(arg0, arg1, arg2);
    }

    @Override
    public void addTouchables(ArrayList arg0) {
        viewPager.addTouchables(arg0);
    }
...2600+ lines more of the same like
How I have reached to this code:

  1.  I am using Eclipse ADT.
  2. After I defined the constructors and the setter, I went on the bottom of the class ALT + SHIFT + V (override methods), selected them all and then created the needed overrides.
  3. After that I did simple string replacement of `super.` with `viewPager.` and I got the result above. 
Now, however I was faced with a lot of errors of the kind:


This was logical - I am not able to decorate protected methods. So I went over and deleted all the protected methods (I did not know how to do that automatically so it was quite frustrating a work). Then I did one more change visible in the following version of the decorated pager:

public class DecoratedViewPager extends ViewPager {
    private ViewPager viewPager;

    public DecoratedViewPager(Context context, ViewPager viewPager) {
        super(context);
        this.viewPager = viewPager;
    }

    public DecoratedViewPager(Context context, AttributeSet attrs, ViewPager viewPager) {
        super(context, attrs);
        this.viewPager = viewPager;
    }

    @Override
    public void addFocusables(ArrayList arg0, int arg1, int arg2) {
        if (viewPager != null) {
            viewPager.addFocusables(arg0, arg1, arg2);
        } else {
            super.addFocusables(arg0, arg1, arg2);
        }
    }

    @Override
    public boolean arrowScroll(int arg0) {
        return (viewPager != null) ? viewPager.arrowScroll(arg0) : super.arrowScroll(arg0);
    }
    
Here, I reminded myself that the decorated `viewPager` will be null while the super constructor runs (actually I did not know by the time, I found out about it later on, but I add the change now for completeness of the code), so I needed to add these `!= null` checks. Thankfully I was able to automate this last change using Ruby, so I got my class consisting of 3100 lines+ automatically. Now I change the definition of the notify out of bounds pager to be using this decoration and we get to the second tag in the repository "decorator_defined".

Hooking up the decorator

Note that as a result of the previous section we still have not completed the refactoring of the application we develop - we have defined the decorator, but we have not actually started using it in our application. What I needed was a way to construct the decorated view pager at runtime (because some configurations, e.g. the listeneres were done at runtime) and then place it in the layout. Actually I was not entirely clear how to do that, so I posted a question in StackOverflow and soon a suggestion came that looked promising: I had to use `LayoutInflater.setFactory` which would allow me to place runtime constructed instances instead of layout-defined. Nice :)
So I went there and added the following in my fragment:
    private class ViewPagerFactory implements Factory {
        private static final String VIEW_PAGER = "android.support.v4.view.ViewPager";

        private Factory mBaseFactory;

        public ViewPagerFactory(Factory factory) {
            this.mBaseFactory = factory;
        }

        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            if (name.equals(VIEW_PAGER)) {
                ViewPager viewPager = new ViewPager(context, attrs);
                return new NotifyOutOfBoundsViewPager(context, attrs, viewPager);
            } else {
                if (mBaseFactory != null) {
                    return mBaseFactory.onCreateView(name, context, attrs);
                } else {
                    return null;
                }
            }
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        inflater.setFactory(new ViewPagerFactory(this.getActivity()));
        return inflater.inflate(R.layout.sample_pager_fragment, container, false);
    }
Here I define a custom factory, that replaces only the ViewPagers with my custom, runtime-constructed instance. It looks fine, but when I ran the application with these changes I got the following error:
"java.lang.IllegalStateException: A factory has already been set on this LayoutInflater" and my application crashed. Short search and I found a post explaining both the cause of the problem and the "solution". Well, after reading the explanation, I was clear that I might be causing some issues with the compatibility replacing the support layout inflator factory with my own, but I just wanted to see what happens. So my method became:
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        try {
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(inflater, false);
            inflater.setFactory(new ViewPagerFactory(this.getActivity()));
        } catch (IllegalAccessException e) {
            Log.e(LOG_TAG, "Error while tweaking the inflator factory");
            throw new RuntimeException(e);
        } catch (IllegalArgumentException e) {
            Log.e(LOG_TAG, "Error while tweaking the inflator factory");
            throw new RuntimeException(e);
        } catch (NoSuchFieldException e) {
            Log.e(LOG_TAG, "Error while tweaking the inflator factory");
            throw new RuntimeException(e);
        }
        return inflater.inflate(R.layout.sample_pager_fragment, container, false);
    }
The code including this change can be seen in tag "decorator_integrated_v1". Finally! Yes! I was able to run the application without any further immediate crashes. Yes, but... nothing was loaded in the pager itself. Why? Well, I came to the conclusion quite fast - I am setting the adapter to the decorator, but it delegates to the widgets it wraps. Thus the adapter is actually set to a ViewPager, that is not the one placed in the layout. Oh gosh, how confusing! I thought that things might get fixed if I make all the decorated setters set in both instances (wrap and current instance) or only in the current instance. None of those worked and I got even more confused. I was already feeling that I was building house of cards, placing the most narrow level at the bottom:

  • the data was not placed in the correct view pager
  • there was the problem with nulls of the decorated viewPager while constructing the decorator
  • I had to give up on decorating the protected methods earlier already
  • I was not sure that my solution would combine with the support library at all.
  • I had a class with thousands of lines!
Thus, it was the time to confess to myself that this might not be the best approach to it all.

The working solution

So I reflected again and finally I came up with the following conclusion: It is wrong to try to construct full-blown Android views for the purpose of decoration. If I did I came to be with just too many methods purposed for the same thing (say setAdapter) and I was also faced with the need to replace runtime constructed widget in the layout. Rather I decided that I need to only add the decorations at runtime to a widget defined in the layout. How did I do that?
public class DecoratedViewPager extends ViewPager {
    private ViewPagerDecoration decoration;

    public DecoratedViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public DecoratedViewPager(Context context) {
        super(context);
    }

    public void setDecoration(ViewPagerDecoration decoration) {
        this.decoration = decoration;
        decoration.setViewPager(this);
        decoration.decorateConstructor(this);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (decoration != null) {
            decoration.decorateOnInterceptTouchEvent(event);
        }
        return super.onInterceptTouchEvent(event);
    }
You see how beautiful it gets - I define the custom view `DecoratedViewPager` that possibly aggregates `ViewPagerDecoration`. It will define decorations of the pager api (like we do with `decoration.decorateOnInterceptTouchEvent`). Here is how the definition of this method looks like in the decoration:
    private ViewPagerDecoration viewPagerDecoration;
    /**
     * Adds logic to the on intercept touch event of the view pager.
     * 
     * @param event the event that was intercepted.
     */
    public void decorateOnInterceptTouchEvent(MotionEvent event) {
        if (viewPagerDecoration != null) {
            viewPagerDecoration.decorateOnInterceptTouchEvent(event);
        }
    }
The cool thing about this approach is that in such way we can easily decorate even protected methods of the view pager and we need to define methods in the decoration and delegations in the decorator only when the need comes (thus no more thousand lines classes). Last, but not least, I also provide a cool way to decorate even the listeners we set for the decorated pager:
    /** Decoration for {@link OnPageChangeListener} that can be extended in the further extending classes. */
    protected class DecoratedOnPageChangeListener implements OnPageChangeListener {
        private OnPageChangeListener pageChangeListener;

        public DecoratedOnPageChangeListener(OnPageChangeListener pageChangeListener) {
            this.pageChangeListener = pageChangeListener;
        }

        @Override
        public void onPageSelected(int position) {
            if (pageChangeListener != null) {
                pageChangeListener.onPageSelected(position);
            }
        }

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            if (pageChangeListener != null) {
                pageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            if (pageChangeListener != null) {
                pageChangeListener.onPageScrollStateChanged(state);
            }
        }
    }

    /**
     * Decorates the given listener
     * 
     * @param listener The on page change listener to decorate.
     * @return The decorate on page listener
     */
    public OnPageChangeListener decorateOnPageChangeListener(OnPageChangeListener listener) {
        if (viewPagerDecoration != null) {
            return viewPagerDecoration.decorateOnPageChangeListener(listener);
        } else {
            return listener;
        }
    }
With the method in `ViewPagerDecorationTimer` being defined as follows:
    private class TimerViewOnPageChangeListener extends DecoratedOnPageChangeListener {

        public TimerViewOnPageChangeListener(OnPageChangeListener pageChangeListener) {
            super(pageChangeListener);
        }

        @Override
        public void onPageSelected(int position) {
            updateTimeRemainingUI(timer.getRemainingMillis());
            super.onPageSelected(position);
        }
    }

    @Override
    public OnPageChangeListener decorateOnPageChangeListener(OnPageChangeListener listener) {
        return super.decorateOnPageChangeListener(new TimerViewOnPageChangeListener(listener));
    }
With such approach we can seamlessly apply multiple decorations even to the listeners we set for the view pager. There is a tag "working_solution" in the repository with this solution shown working. Check it out and you will see how I imagine decoartion of Android views should work. Oh, and did I mentioned that I also provide additional method in my decorated view pager for the so-longed-for method `getCurrentView`?

Hopefully this post would help you write your android application with better architecture. I, personally, do not see a reason why all extensions to views would not happen as decorations, as, good practise advises us, not to put any business logic in them.

2 коментара:

  1. A problem with this way of view decoration is that class from which decorator extends is a concrete class with some calculations in constructors (ViewPager in example). So each concrete instance a bit slows down an entire process. Why does google not provide interfaces for its views? I don't know..

    ОтговорИзтриване
  2. I believe in your onCreateView() you are using reflection with `getDeclaredField` method. Does this have any runtime performance penalty?

    ОтговорИзтриване