A Cleaner MVC Inspired by Continuation-Passing Style
4. Going asynchronous with callbacks
Adopting an asynchronous message passing style is an elegant solution to the threading problem. In practice, we guarantee that getNewsFeed() completes immediately, even when the result is not ready; the caller will be notified later, by means of a callback that is passed as argument:
public static interface AvailabilityNotifier
{
public void notifyFeedAvailable (@Nonnull RssFeed newsFeed);
public void notifyCachedFeedAvailable (@Nonnull RssFeed newsFeed);
public void notifyFeedUnavailable();
public void notifyFeedCouldBeDownloaded (@Nonnull DownloadConfirmation confirmation);
}
AvailabilityNotifier will be called back in one of its four methods for one of the four possible outcomes:
-
notifyFeedAvailable() notifies that a fresh feed is available and carries it as a parameter;
-
notifyCachedFeedAvailable() notifies that a cached feed is available (for some reason a fresh copy couldn't be retrieved) and carries it at as parameter;
-
notifyFeedUnavailable() notifies that it is not possible to retrieve any data;
-
notifyFeedCouldBeDownloaded() notifies that there's no data available, but it could be possible to download it by connecting to the network.
Of course, the presence of a specific callback method for each outcome also eliminates the need of a switch. Should a future version of NewsService allow a fifth outcome, our code won't be compilable unless we provide the implementation of a fifth method, forcing us to take care of the new behavior. Or, perhaps, it will be possible to provide a default implementation of the new method (thus turning AvailabilityNotifier into an abstract class) to have backward compatibility with existing code. We would be safe in both cases.
Now, let's focus on the fourth method, notifyFeedCouldBeDownloaded(): it's the one notifying that the data could be downloaded, upon confirmation. As you can see, it has an argument, called DownloadConfirmation. It's another callback:
public static interface DownloadConfirmation
{
public void confirmDownload();
}
The exposed single method confirmDownload() allows to complete the operation (of course, if the user cancels the operation, the method won't be called). doDownload() works again asynchronously, returning immediately (so it can be called by a UI thread); when the operation is completed, with a positive or negative outcome, the initial instance of AvailabilityNotifier will be notified again. So, instead of exposing a getNewsFeedWithoutConfirmation() into NewsService, warning that it shouldn't be called but under some circumstances, confirmDownload() is available on an object that only materializes, and thus can be called, precisely under those circumstances.
Let's think of these callbacks as objects representing a state in the the control flow of the application: each state only exposes methods doing things that are allowed to be done in that state. A great reduction of the chances of an error.
Note that the implementation of DownloadConfirmation could even enforce a check in confirmDownload() so it can be called only once per instance (and perhaps within a certain time window). It would prevent tricks such as keeping a reference to DownloadConfirmation to call confirmDownload() multiple times during other interactions. Sure, we can't automatically enforce the proper implementation in NewsViewController (after all it could just pretend the user has confirmed without asking anything), but we've made some reasonable efforts to avoid some trivial errors.
Below is a diagram of the News API, where you can see the relationships among the classes described so far:
![]() |
Figure 2. The News API
We can now have a look at the implementation of the method getNewsFeed() in NewsService (I'm not showing the full listing, but methods names are self-describing):
@Override
public void getNewsFeed (final @Nonnull AvailabilityNotifier availabilityNotifier)
{
try
{
availabilityNotifier.notifyFeedAvailable(cache.getRssFeed());
}
catch (NotFoundException e)
{
ensureCacheIsInitialized(availabilityNotifier);
if (!cache.getStatus().isDownloadNeeded())
{
readNewsFeedAndNotifyAvailability(availabilityNotifier);
}
else if (preferences.get().isNewsDownloadAllowed())
{
downloadNewsFeedAndNotifyAvailability(availabilityNotifier);
}
else
{
availabilityNotifier.notifyFeedCouldBeDownloaded(new DownloadConfirmation()
{
public void confirmDownload()
{
downloadNewsFeedAndNotifyAvailability(availabilityNotifier);
}
});
}
}
}
| Attachment | Size |
|---|---|
| it.tidalwave.bluebill.mobile.news_.png | 32.91 KB |
| it.tidalwave.bluebill.mobile.news_.ui_.png | 13.07 KB |
| package.png | 12.46 KB |
| UserNotificationWithFeedback.png | 21.21 KB |
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)






