An Evaluation of aVoice

aVoice is a Google Voice client for Windows 8. It makes use of some neat interaction paradigms:

The video doesn’t show pinch-to-zoom in, but that’s also possible. In the world of touch, these interaction paradigms are pretty new and they aren’t yet mainstream. I’ll tell you how they’re working for aVoice and some pitfalls to avoid.

Semantic Zoom

I’ve seen two uses for it over several platforms:

  • A jump list – Windows Phone’s app list is grouped alphabetically. The semantic zoom gets you to apps that start with Z quickly.
  • An information filter – Windows 8’s start screen zooms out to let you see a simpler big picture. Note that this is different from Android’s MIUI ROM launcher, which is simply optical zoom.

aVoice uses the information filter semantic zoom.

When zoomed in, a single message thread gets priority so that the user can see the conversation's history and respond accordingly.
When zoomed in, a single message thread gets priority so that the user can see the conversation’s history and respond.
When zoomed out, a message thread is reduced to a single box with a snippet of the most recent message. The color of the box indicates who sent the last message.
When zoomed out, a message thread is reduced to a single box with a snippet of the most recent message. The color of the box indicates who sent the last message.

A heuristic I think applies heavily to the information filter semantic zoom is recognition rather than recall (#6). The Windows 8 start screen does a great job of letting the user recognize what app they’d like in the zoomed out view by creating the illusion that the user is merely optically zooming out, but being a bit smarter. They disable live tiles and replace them with a small version of the app icon:

comparison2

 

Above, on the upper left is a screenshot of what the Finance app looks like in the zoomed out view (62px x 30px). On the right is what it would look like if the Finance app’s live tile was left on and simply shrunk. Obviously, the left retains meaning while the right does not.

This trick let’s the user recognize where they are spatially and recognize an app by its color and icon.

Unfortunately, aVoice doesn’t work the same way. When zoomed in, messages are in a horizontal stack. When zoomed out, they are in a grid, similar to the start screen. This breaks spatial recognition, and probably explains why I observed friends exclaiming “Wait, where was I?” when using aVoice. In a redesign, I would probably scrap the semantic zoom. It is ill suited for this case. So use caution when considering the information filter style. It could end up being more confusing than helpful.

Swipe to Select

Press-and-hold is the standard selection pattern for iOS (jiggle mode) and Android, but there’s plenty of debate about it.

Swipe to select takes a different approach, allowing users to flick and item down or up to select it. It’s significantly faster than press-and-hold (check out the video here, under Swipe to select, command, and move). However, swipe to select has some issues. Here are a few that I’ve seen with aVoice:

  • By virtue of being different, most people attempt to press-and-hold and are confused when it doesn’t work
  • Users attempting to quickly pan may accidentally select instead

Press-and-hold results in a hint that you should swipe down, but still doesn’t select the item. This goes against the consistency and standards (#4) of the popular market and ideally aVoice would allow both methods.

Error recovery (#9) is super important to keep a stress-free experience. The accidental selections while panning is worrisome, but fortunately recoverable via the Clear Selection button. If you use swipe to select, be careful about letting it do more than select because the action is frequently unintentional.

The Forgiving User

RingPack just underwent a major overhaul. With large amounts of code churn, it starts to become likely that new bugs will be introduced (especially Android’s fragmentation problem). This update was no exception — within 3 hours of pushing a live update, I saw a new crash, a few angry reviews, and one very upset email. Yikes!

1

There’s a powerful lesson here. Budget time to rapidly respond to user issues on a new release. Luckily, I did. The telemetry data from the Android Developer Dashboard and Google Analytics let me respond within hours.

2

The surprising thing is how quick users are to forgive once they realize you’re willing to work with them. Even something so impersonal as replying to their review does a lot to diffuse the situation. Of course, your mileage may vary depending on how critical your app is to the user’s life.

Adoption

I was curious how quickly people would upgrade. The answer is — not very quickly.

3
This is 48 hours from the launch of version 8. The subsequent versions are minor bug fixes.

Roughly 700 users saw the update and gave it a try. Amazingly that’s only about 5% of the user base. I see that as both good and bad. On one hand, lazy updating means more users with a sub-par experience. On the other, it’s nice that only 5% hit the nasty crash in version 8.

In conclusion,

  • Budget time to rapidly iterate
  • Be responsive and respectful to all users (even the angry ones)
  • Love your telemetry

Following these rules can quickly reverse the damage done by a rocky launch.

 

 

IDisposable for Dummies

There are multiple explanations for how to use IDisposable. I get it now, so here goes:

public class Connection : IDisposable
{
    public IntPtr buffer; // Unmanaged resource (memory buffer)
    public SafeHandle resource; // Managed resource that implements IDisposable

    private bool disposed;

    public Connection()
    {
        buffer = ... // allocates memory
        resource = new SafeHandle( ... ) // allocates some managed resource that we assume allocates an unmanaged resource, but we don't care
    }

    ~ Connection()
    {
        Dispose(false);
    }

    public void StartConnection()
    {
        if (disposed) throw new ObjectDisposedException();

        ... // implementation
    }

    public void Dispose()
    {
        // NOTE: If you think multiple threads could try to Dispose your object
        // consider locking this section as well. DON'T do this in the Dispose(bool)
        // function, as locks are unsafe at finalization time (and there is only one
        // finalizer thread)
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public virtual void Dispose(bool disposing)
    {
        // No need to repeat the frees
        if (!disposed)
        {
            // Release ALL unmanaged resources here so that it's ALWAYS done!
            Release(buffer);

            if (disposing)
            {
                // If the caller has explicitly called the Dispose() method, call Dispose() on managed objects early.
                // By early, I mean that we TRUST disposable managed objects to do the same thing we are above for UNMANAGED resources

                resource.Dispose(); // Note that we don't have to check if it's NULL first because this will never execute in the Finalized state
            }
        }
    }
}

The complex bit here is the trust that all IDisposable objects behave like this. Basically, an object should never let itself leak an unmanaged resource. Thus, it frees unmanaged resources both when Dispose() is called and on from the Finalizer.