Thursday, June 26, 2014

C++: Tying object lifetimes together with shared_ptr and custom deleters

Once upon a time, when a great man died his slaves would be killed and buried alongside him as gifts to carry into the netherworld. Our civilizations have mostly moved beyond both slavery and grave gifts, but our code has notit is actually quite common to have a series of subordinate helper objects that you want to keep around for exactly as long as some master object is alive. The most straightforward way of doing this, of course, is to have the the slave objects be actual member variables of the master's class. But that won't work if you've made the choice to keep your objects loosely coupled (for any of several excellent reasons). For example, consider a class that allows builders to attach custom behaviors to it using the observer pattern:

class Master
{
public:
   struct IObserver { /* ... */ };

   // WEAKLY attach an observer that will be notified when this object changes
   void AddChangedObserver(std::weak_ptr<IObserver> observer);
};

// write the changes to disk
class Serializer : public Master::IObserver { /* ... */ };

// update UI to reflect changes to the object
class UiUpdater : public Master::IObserver { /* ... */ };

std::shared_ptr<Master> MakeMaster()
{
   auto master = std::make_shared<Master>();

   auto serializer = std::make_shared<Serializer>();
   master->AddChangedObserver(serializer);

   auto uiUpdater = std::make_shared<UiUpdater>();
   master->AddChangedObserver(uiUpdater);    

   return master; // danger!
}

There is a problem here. We want serializer and uiUpdater to stick around as long as the returned Master does, but that requires us to keep a shared_ptr to each somewhere. The code is not doing that now, so both will be deleted (and implicitly deregistered) as soon as MakeMaster() returns. This will not cause any undefined behavior, but we won't get the attached behaviors we were expecting from these objects either.

So where do we keep the shared_ptrs to the two observers? We could return them from MakeMaster() along with the main object, but that's just punting to the caller (who shouldn't need to know that we're attaching behaviors anyway). A better solution is to leverage the ability to give shared_ptr a custom deleter. Instead of return master, we can do this:

std::shared_ptr<Master> master2(master.get(),
    [master, serializer, uiUpdater](Master*) mutable
    {
        master.reset();
        serializer.reset();
        uiUpdater.reset();
    });
return master2;

This requires a bit of explanation. First of all, we are creating a new reference count/shared_ptr "family" over the Master object we created earlier. Normally having more than one reference count for the same object would eventually result in a double-deletion disaster, but that won't happen here. The reason is that we're giving this second shared_ptr a custom deleter lambda that doesn't actually delete anythinginstead, it simply hangs on to "keepalive" shared_ptrs to the three objects via captures. However, when the master2 family's strong reference count reaches zero and this deleter lambda is called, it will reset* all three of themit is this action that will result in the actual deletion of the three objects via the ordinary shared_ptr mechanism. Which is exactly what we want.

*Note: It is important to actually reset the captured shared_ptrs in the deleter and not simply rely on them being destroyed when the lambda is. If this wasn't done, weak_ptrs in the master2 family would be able to keep the objects alive.

Thursday, June 5, 2014

A compatible animated ActionBar refresh icon

Many apps put a refresh icon on the ActionBar, and it's nice to have that icon animate while a refresh is in progress. The internet is full of suggestions on how to do this, but most either require third party libraries (e.g. ActionBarSherlock) or are not compatible with older versions of the API. Here is some simple code that will rotate the refresh icon clockwise for API 4+ (disclaimer: I've only tested it down to API 10).

Note: The code assumes you already have ic_action_refresh in res/drawable. You can get the stock Android refresh icon from the Action Bar Icon Pack here.

res/anim/spin_clockwise.xml:
<?xml version="1.0" encoding="utf-8"?>

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:fromDegrees="0"
    android:interpolator="@android:anim/linear_interpolator"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toDegrees="360" />

res/layout/refresh_action_view.xml:
<?xml version="1.0" encoding="utf-8"?>

<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:contentDescription="Refresh"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_action_refresh"
    android:paddingLeft="12dp"
    android:paddingRight="12dp"
    />

AnimatingRefreshButtonManager.java:
import android.content.Context;
import android.support.v4.view.MenuItemCompat;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;

public class AnimatingRefreshButtonManager
{
    private final MenuItem mRefreshItem;
    private final Animation mRotationAnimation;

    private boolean mIsRefreshInProgress = false;

    public AnimatingRefreshButtonManager(Context context,
       MenuItem refreshItem)
    {
        // null checks omitted for brevity

        mRefreshItem = refreshItem;
        mRotationAnimation = AnimationUtils.loadAnimation(
            context, R.anim.spin_clockwise);

        mRotationAnimation.setAnimationListener(
            new Animation.AnimationListener()
            {
                @Override public void onAnimationStart(Animation animation) {}

                @Override public void onAnimationEnd(Animation animation) {}

                @Override public void onAnimationRepeat(Animation animation)
                {
                    // If a refresh is not in progress, stop the animation
                    // once it reaches the end of a full cycle
                    if (!mIsRefreshInProgress)
                        stopAnimation();
                }
            }
        );

        mRotationAnimation.setRepeatCount(Animation.INFINITE);
    }

    public void onRefreshBeginning()
    {
        if (mIsRefreshInProgress)
            return;
        mIsRefreshInProgress = true;

        stopAnimation();
        MenuItemCompat.setActionView(mRefreshItem,
            R.layout.refresh_action_view);
        View actionView = MenuItemCompat.getActionView(mRefreshItem);
        if (actionView != null)
            actionView.startAnimation(mRotationAnimation);
    }

    public void onRefreshComplete()
    {
        mIsRefreshInProgress = false;
    }

    private void stopAnimation()
    {
        View actionView = MenuItemCompat.getActionView(mRefreshItem);
        if (actionView == null)
            return;
        actionView.clearAnimation();
        MenuItemCompat.setActionView(mRefreshItem, null);
    }
}

Skeleton Activity demonstrating usage:
import android.app.Activity;
import android.view.Menu;
import android.view.MenuItem;

public class SomeActivity extends Activity
{
    private AnimatingRefreshButtonManager mRefreshButtonManager;
    
    @Override public boolean onCreateOptionsMenu(Menu menu)
    {
        getMenuInflater().inflate(R.menu.main, menu);

        MenuItem refreshItem = menu.findItem(R.id.action_refresh);

        mRefreshButtonManager =
           new AnimatingRefreshButtonManager(this, refreshItem);

        return super.onCreateOptionsMenu(menu);
    }
    
    @Override public boolean onOptionsItemSelected(MenuItem item)
    {
        switch (item.getItemId())
        {
            case R.id.action_refresh:
                mRefreshButtonManager.onRefreshBeginning();
                // start the refresh
                return true;
        }

        return super.onOptionsItemSelected(item);
    }
    
    // how you detect this is application-specific
    void onRefreshComplete()
    {
        mRefreshButtonManager.onRefreshComplete();
    }
}