Introduction

La-la-la not looking; nothing changed :)

‚Ēą ūüôĄūü§Ē¬†Maybe not what we want ¬ģ

This post is a continuation of one of my previous posts, ‚ÄúMessing with the drawable state‚ÄĚ. In this post we will continue with the drawable wrapper we created, discuss some of its¬†shortcomings and make it even better.

The flaw

The class we created last time has one flaw: It does not handle the view's state at all. So in case we have a drawable that should also responds to the selected, or¬†touched state, it won't work. The reason it won't work is the fact that we just returned false from the setState method. Our current implementation¬†means something like, ‚Äúla-la-la not looking; nothing changed¬†:)‚ÄĚ. That may not be what we want.

Possible solution: custom merge strategy

Now suppose we would like to create the most flexible implementation possible ('cause we can), what would that implementation consist of? The only thing missing in our drawable-wrapper is merging the view's state with our custom state. We will make it so that our client can provide us with a custom merge strategy. That is our only requirement. Let's first think about this interface and define it. Remember that although we want to be flexible it should also be simple to understand. Obviously the client will need the two states it has to merge; the view-state and our custom state. And the client  also needs to set that on something.

                
public interface MergeStrategy {
    /**
     * Merges the provided states. Must call
     * drawable#applyMergedState(int[]) to
     * apply the new state.
     */
    void mergeDrawableStates(CustomStateDrawable d,
            @Nullable int[] viewState,
            @Nullable int[] customState);
} 
            

From a different perspective, we also don't want to force the client to implement a MergeStrategy, the client must be able to use our class without the need to create one. Actually it is something only ‚Äúpower clients‚ÄĚ would want to use. So we should definitely have a default implementation as well.

Providing sensible defaults

In this case we could choose to use static methods for this. We would like to have one that creates an instance without a merge strategy; one that works exactly like the one we have in part one of this post. And, we could choose to have a static method that simply creates in instance that merges the states as you would normally want to do this. This could look like this:

                
static BaseMergeStrategy sBaseMergeStrategy;

MergeStrategy mMergeStrategy;

private StateDrawableWrapper(Drawable drawable, MergeStrategy s) {
    super(drawable);
    mMergeStrategy = s;
}

/**
 * Creates an instance that ignores the view-state
 * and only uses the custom state.
 */
public static StateDrawableWrapper wrapNoViewState(@NonNull Drawable d) {
    return new StateDrawableWrapper(d, null);
}

/**
 * Creates an instance that merges the view-state
 * with our custom state
 */
public static synchronized StateDrawableWrapper wrapWithViewState(@NonNull Drawable d) {
    if (sBaseMergeStrategy == null) {
        sBaseMergeStrategy = new BaseMergeStrategy();
    }
    return new StateDrawableWrapper(d, sBaseMergeStrategy);
} 
            

In above implementation you might notice the following:

  1. The private constructor.
  2. The static sBaseMergeStrategy / wrapWithViewState method.
  3. The wrapNoViewState method

Let's discuss these choices!

Private constructor

The first interesting thing is the private constructor. Instead of letting the user call the constructor and having multiple variants of this, I have chosen to use more descriptive static factory methods. If we wouldn't do it like this, we'd end up with two constructors that have the same signature:

                
StateDrawableWrapper(Drawable wrapped); 
            

So because we use static factory methods, the name makes clear what it does and we no longer have those conflicting constructors. Just one constructor that all of the factory methods use.

The static merge strategy field

The static merge strategy is actually an optimization for Android, and very much related to the wrapWithViewStateMethod. This makes sure we don't create multiple instances and we don't create instances that we do not need. As you can see, this method has also been marked synchronized. This is a threading thing and makes sure that all threads can read and write the variable in the correct state. The details about this are out of scope for this post. As this method creates an instance that respects the view-state, we (cleverly) name this method wrapWithViewState. This makes sure all clients understand what this method does.

The wrapNoViewState method

Finally there is this method that creates in instance that behaves exactly like the one we used before. To differentiate between this method and the wrapWithViewState, this method is named wrapNoViewState.

The missing method

The only thing we do not yet have, is a factory to actually create an instance with a custom strategy implementation. We will name this method wrap. This method looks something like this:

                
public static StateDrawableWrapper wrap(
        @NonNull DrawableWrapper d, @Nullable MergeStrategy s) {
    return new StateDrawableWrapper(d, s);
} 
            

One last thing

You thought we were done right? Well, we aren't. There is one final thing to improve. Let's look at this interface again:

                
public interface MergeStrategy {

    /**
     * Merges the provided states. Must call drawable#applyMergedState(int[]) to
     * apply the new state.
     */
    void mergeDrawableStates(CustomStateDrawable d,
            @Nullable int[] viewState, @Nullable int[] customState);
} 
            

So, what is that final thing that is bothering me? Right now, the client needs to apply the merged state manually to the drawable. I think it would be better if we can hide that detail from the client. Drawable's setState method looks like this:

                
public boolean setState(@NonNull final int[] stateSet)
            

And its javadoc looks like this:

If the new state you are supplying causes the appearance of the Drawable to change, then it is responsible for calling invalidateSelf() in order to have itself redrawn, and true will be returned from this function.

Above snippet means that our wrapper must call invalidateSelf in case the state changes because of the new state. For our wrapper it is important that this is implemented correctly and that a misbehaving client does not break things. More importantly the strategy we require the client to provide should be clean and pure. So, no additional steps or responsibilities to take. Putting this responsibility at the client clearly violates this principle. So that would mean we should be in control.

Circular dependencies and control

Now what would give us more control? Should we let the client calculate the state and set it? Or should we ask the client for the new state and set it ourselves? The latter obviously gives us more control and limits the responsibility of the client. And there is actually another architectural problem. The interface now knows about the CustomStateDrawable class and the CustomStateDrawable also knows about this interface. This causes a circular dependency which is far from elegant. So what can we do about this? Well first, we have to remove that circular dependency. Circular dependencies can normally be solved by using an interface or by using a proper return type. This second solution also matches with our control requirement. So this would solve both of our problems. Our interface would then look like this:

                
public interface MergeStrategy {
    int[] mergeDrawableStates(@Nullable int[] viewState, @Nullable int[] customState);
} 
            

Now we have a loosely coupled interface, that just merges two arrays. And this is completely optional. The static wrap methods just take care of everything for normal operation. I will write about the other approach, using an interface to prevent the cyclic dependency another time. Now one last thing...

Who would ever want to use a custom implementation?

So what would be a use-case where the client would want to use a custom wrapper? I can only think of one (far-fetched) scenario, let's say certain of your own states conflict with the view's own states or amend those states. For example when something should always show as checked because it is a default option that the user can't unset. And for some reason this has to be solved in the drawable state (there are better ways to solve this). Then a client could theoretically add or remove additional states from the view to satisfy these conditions.

I hope you liked this post. Let me know in the comments.
Thanks for reading!

Cheers, Nick