Category Archives: Tips
An Evaluation of aVoice
aVoice is a Google Voice client for Windows 8. It makes use of some neat interaction paradigms:
- Semantic Zoom
- Swipe to Select (a.k.a Cross-Slide)
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.
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:
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!
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.
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.
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.
Introducing SortingObservableCollection
ObservableCollections are awesome. The two way binding lets your user seamlessly update your model and any programmatic changes to the model will be seamlessly reflected in the view.
I wanted one that kept it’s items in order, no matter what. It should maintain order as items are added and watch them via INotifyPropertyChanged to move them automatically. So here it is on CodePlex and NuGet. Sample usage is:
var collection = new SortingObservableCollection<MyViewModel, int>(Comparer<int>.Default, model => model.IntPropertyToSortOn); collection.Add(new MyViewModel(3)); collection.Add(new MyViewModel(1)); collection.Add(new MyViewModel(2)); // At this point, the order is 1, 2, 3 collection[0].IntPropertyToSortOn = 4; // As long as IntPropertyToSortOn uses INotifyPropertyChanged, this will cause the collection to resort correctly
The Hidden “gotcha” in Windows Store App Lifecycle
The application lifecycle diagram from MSDN:
Pretty simple, but that Resuming bit is the “gotcha”. MSDN doesn’t really explain how this lifecycle relates to code as much as I’d like, so I’ll write it in layman’s terms:
- For your Application class:
- OnLaunched will be called any time your app is not in memory or if the app is being asked open something (see the ActivationKind enum)
- The Suspending event will fire whenever your app leaves the view
- The Resuming event will fire whenever your app re-enters the view, only if OnLaunched wouldn’t be called
- For the Page class:
- OnNavigatedTo will be called whenever someone calls Frame.Navigate with your page. This could be explicitly via a code-behind call, or implicitly via a call from the SuspensionManager helper class
- OnNavigatedFrom will be called whenever your app leaves the view by the SuspensionManager (in sync with the Application’s Suspending event)
(NOTE: Visual Studio’s templates come with the NavigationHelper.cs class. You’re encouraged to ignore the OnNavigatedTo and OnNavigatedFrom overrides, instead using NavigationHelper’s LoadState and SaveState events)
You should see one basic thing missing from Page. It can’t know about the Resuming lifecycle event. This creates a bit of an awkward situation. Let’s say you’re in MainPage.xaml.cs and you need to register for some events on OnNavigatedTo with an object that you were passed as a navigation parameter. If you don’t de-register these events on OnNavigatedFrom, you’ll be sorry. It’s possible for another call to OnNavigatedTo to occur on the same instance of MainPage — you’ll register for the same event twice which is probably not what you expected.
So OK, de-register your events in OnNavigatedFrom. But wait, that doesn’t quite work either! Remember that OnNavigatedFrom is called every time your app leaves the view, but OnNavigatedTo might not be called when it returns. You’ll end up without event handlers if the user leaves the app then returns while it’s still in memory (extremely common case).
So what do you do?
Honestly, the Page class isn’t enough. We need to extend it. Try something like this:
/// <summary> /// Use in place of the Page class for better application lifecycle support and better /// code reuse. /// </summary> public class BasicPage : Page { #region Public Properties /// <summary> /// Reference to the underlying NavigationHelper, which can be accessed by the Application. /// </summary> public NavigationHelper NavigationHelper { get; private set; } /// <summary> /// Use for the DataContext. /// </summary> public ObservableDictionary DefaultViewModel { get; private set; } #endregion #region Constructors /// <summary> /// Inheriting classes should call InitializeComponent(). /// </summary> public BasicPage() { NavigationHelper = new NavigationHelper(this); NavigationHelper.LoadState += LoadState; NavigationHelper.SaveState += SaveState; NavigationHelper.ResumeState += ResumeState; DefaultViewModel = new ObservableDictionary(); } #endregion #region Protected methods /// <summary> /// Called when this page should initialize it's data for the first time. Register event handlers. /// </summary> protected virtual void LoadState(object sender, LoadStateEventArgs e) { } /// <summary> /// Called when this page should save it's data to disk in preparation for leaving memory. Deregister /// event handlers. /// </summary> protected virtual void SaveState(object sender, SaveStateEventArgs e) { } /// <summary> /// Called when this page has returned to the view. Register event handlers if necessary or update the view. /// </summary> protected virtual void ResumeState(object sender) { } protected override void OnNavigatedTo(NavigationEventArgs e) { NavigationHelper.OnNavigatedTo(e); } protected override void OnNavigatedFrom(NavigationEventArgs e) { NavigationHelper.OnNavigatedFrom(e); } #endregion }
The basic mechanics for how to hook up a BasicPage like this can be found in a similar blog post. The only real difference between the above example and the linked one is the ResumeState method. This requires a modification to NavigationHelper.cs as well. Add the following:
/// <summary> /// Register this event on the current page to react to the app coming /// back into view. This event and LoadState are mutually exclusive, /// you will never get both. /// /// You should only need to do view updates, as everything should still /// be in-memory when this event is called. /// </summary> public event ResumeStateEventHandler ResumeState; ... /// <summary> /// Invoked when the app is resuming. /// </summary> public void OnResuming() { if (this.ResumeState != null) { this.ResumeState(this); } }
The final hook is in your App.xaml.cs code-behind, you need to register for the Resuming event and pass it to the page:
this.Resuming += OnResuming; ... private void OnResuming(object sender, object e) { Frame f = Window.Current.Content as Frame; BasicPage p = f.Content as BasicPage; if (p != null) { p.NavigationHelper.OnResuming(); } else { throw new Exception("Every page must implement BasicPage!"); } }
Now you have the tools you need with minimal code reuse.