loading...

How closely do you adhere to your architecture: Navigation edition

autonomousapps profile image Tony Robalik ・3 min read

(Note: I am not talking about the new Navigation Controller announced at Google I/O. If you think I should be, you're welcome to tell me in the comments!)

As I mentioned in this post, Chess.com is rewriting its Android app from scratch. As part of that process, we've spent a lot of time thinking about the architecture we want, and how we want various screens to relate to one another.

In the current production app, we follow essentially a "one Activity, multiple Fragment" approach, embedded in a traditional Ball of Mud (or Plate of Spaghetti, if you prefer) architecture. And over the past couple of years, we have experimented with various architectural patterns under the rubric "refactoring" (an overused word if ever there was one). We have used at least three different variants of MVP (all hand-rolled), but lately we have ended up settling on a kind of MVVM with heavy use of the Android Architecture Components, particular ViewModels and LiveData. They are really, really great — and unit testable! If you use the app at all, you may be interested to learn that the upcoming version (3.7, in beta soon) will have several new features all written with ViewModels, LiveData, and even the Paging library: Arena tournaments, News, and Achievements.

Our experience with ViewModels and friends has led us to decide that our "v4" app's architecture will rely heavily on them. One particular area of concern is Navigation. In the current prod app, essentially any screen can navigate to any other screen, and the chaos is real.

How v4 is handling navigation

We have made navigation a top-level concern and are beginning to write the abstractions to manage it. We have a Router class that implements a number of interfaces (HomeRouter, LoginRouter, etc) that clearly define how each screen is tied to other screens. And thanks to some clever work with Dagger, the Router class's methods can be named things like goToWelcome() and have no parameters, and the router can handle everything from there. For example:

@ActivityScoped
class Router<T : BaseActivity> @Inject constructor(
    private val activity: T
) : SplashRouter, LoginRouter, OnboardingRouter, HomeRouter {

  override fun goToWelcome() {
    // startActivityAndFinish() is an extension function
    activity.startActivityAndFinish(
        OnboardingActivity.getStartIntent(activity),
        activity.crossfadeAnimation()
    )
}

We're injected a scoped activity that lets us handle everything right in this class and also prevents accidental memory leaks.

This is all well and good, but it finally brings me to the point of this post: how far do we take this?

What about 'back' and 'up' navigation?

At the moment, we're letting the framework handle these two forms of navigation. If a user presses the back button, super.onBackPressed() is called (with a few minor caveats). We also let the framework handle 'up' navigation by setting the parent activity in the manifest and enabling up navigation on the action bar.

But... could we do more? Clearly this is now a schizophrenic system. In some cases, we are manually wiring together activities (and some fragments), while in others, the framework handles this "automatically". Maybe the following would be better:

override fun onBackPressed() {
  viewModel.onBackPressed()
  // no call to super!
}

and similarly we could capture 'up' nav events and manually handle them.

What do you think?

Posted on by:

Discussion

markdown guide