More than a year ago, the Android team at Lyft transitioned from using Fragments to plain Android views. A few months ago, we open sourced Scoop, a framework that lays the foundation for this transition. In this post I’ll elaborate on the decision to transition to a view only framework, and then I’ll provide a brief introduction to Scoop so you can build your own view only apps.

The foundation of an architecture

Since the beginning, the Lyft Android team has faced challenges instantiating more than one Activity because two core components of our app’s design, the map and the navigation drawer, posed technical limitations that precluded using more than one Activity. Let’s examine each individually.

The Map

Ride Request Screen

The ride request screen is something that all of our customers are quite familiar with. This screen hosts a map, and some content layered on top of it exhibiting different states of our ride flow.

If we instantiate the map more than once, our animations would drop frames because we would be consuming too much memory for a smooth transition, and therefore inevitably degrade the user experience. As a result, we only have one instance of the map, with various views layered on top of it.

The Navigation Drawer

Navigation Drawer

Our navigation drawer is not much different than similar navigation menus on other apps. When you click an item in the drawer, the screen swap is reflected in the main view. We couldn’t use activities to implement this behavior because instantiating multiple activities to display views would break the drawer animation. We narrowed our choices to either views or Fragments, and moved towards a decision.

Back in 2012, Android Fragments were popular within the Android community. They were designed to work with tablets, phones, and screens of all sizes, and to allow developers to decompose an app’s UI into small reusable components. So we evaluated Android Fragments and decided to build our application as a single activity with screens represented as fragments (and child fragments in more complicated scenarios).

Back to basics.

As the complexity of that application grew, we began to realize that the extensive usage of Fragments came at a high cost. The Fragment lifecycle was becoming more and more difficult to manage and created bugs that were difficult to debug.

The Android Fragment Lifecycle

Other problems occurred due to what we call “Schrödinger’s Fragment” where you cannot immediately retrieve a reference to your Fragment back from the Fragment manager, which led to a race condition when you’d want to use the returned fragment but it wasn’t there yet. Square’s Pierre-Yves Ricau has a great article illustrating further pitfalls of Fragments here.

Because of these issues, we decided to research other alternative solutions. We landed on a solution that has been there since the beginning — plain Android views.

Views provided us with numerous benefits. They have a simple lifecycle with full control over instantiation, and we could remove the issues around race conditions occurring with Fragment transactions. However, views do sacrifice backstack and navigation support. In order to completely remove Fragments from our app, and focus solely on views, we needed to fill these technical gaps.

Introducing Scoop

We developed an open source view-only framework named Scoop to embrace the benefits of views and address the technical drawbacks of Fragments. Scoop is composed of five key components: Screen, ViewController, Router, UIContainer, and Scopes.

Screen

@ViewController(MyController.class)

@EnterTransition(FadeTransition.class)

@ExitTransition(FadeTransition.class)

public class MyScreen extends Screen {

}

Screen is a metadata object that specifies which view controller to display, optional data to pass to a view controller, and optional transitions between screens. Screens will lay the foundation for your application’s UI.

ViewController

public class MyController extends ViewController {



@Override

protected int layoutId() {

return R.layout.my;

}



@Override

public void onAttach() {

super.onAttach();

}



@Override

public void onDetach() {

super.onDetach();



}



@OnClick(R.id.do_smth)

public void doSomething() {



}

}

ViewControllers manage a portion of the user interface as well as the interactions between that interface and the underlying data. Similar to an activity or a Fragment, ViewControllers require a layout id to render a view that they control. However, ViewControllers do not have a complex lifecycle. The ViewController lifecycle only has two states: “onAttach” and “onDetach”.

Don’t think of ViewControllers as presenters, rather imagine them as less complex ViewGroups with some unique benefits. The goal of introducing ViewControllers was to solve 2 primary issues related to views:

Inheritance

public class MyView extends FrameLayout/LinearLayout/ScrollView {



public MyView(Context context, AttributeSet attr) { } @Override protected void onFinishInflate() { ... }

@Override protected void onAttachedToWindow() { ... }

@Override protected void onDetachedFromWindow() { ... }

}

View are usually inherited from different ViewGroups (LinearLayout, RelativeLayout, etc), so if you want a base class shared amongst ViewGroups you’re pretty much out of luck. While we normally recommend composition over inheritance, sometimes it is more effective to use inheritance when it adheres to Liskov’s substitution principle.

2. Android design preview

If you create a custom view and have some nontrivial logic inside it, Android Studio’s design preview will throw exceptions and fail to render the view. You can add a flag, isInEditMode, to solve this issue, but we found that most developers choose to ignore the flag, and eventually most previews display exceptions similar to the picture above.

Navigation using Router and UIContainer

The Router and UIContainer classes are closely related, and are responsible for navigation between Screens. The Router class maintains the Screen backstack, and provides 5 methods for navigation.

goTo — Go to Screen C and add it to the backstack. replaceWith — Replace the top of the stack with Screen C. replaceAllWith — Replace the entire backstack with Screen C and Screen D. resetTo — Go to Screen C and remove all Screens after it from the backstack. If the Screen C is not in the backstack, remove all and make it the top of the backstack. goBack — Navigate to previous Screen.

When one of these five methods is executed, the Router relays a screen change event to the MainActivity, which is passed to the UIContainer. The UIContainer is responsible for rendering the new (next) view by replacing the active (previous) view, and optionally animating a transition between the two views.

Scope

Scoop’s namesake is the word “scope”. App scopes are similar to ice cream scoops: going deeper in the navigation stack is similar to adding another ice cream scoop to a cone. Scoops exist to provide access to named services, which are simply Java objects, and have limited scopes. For example, the Profile Screen may need to know about your name, password, and favorite music but the Home Screen doesn’t need to know that information. We can say that the Profile Screen’s scope has been limited to only the information it needs to render that particular view, this information is kept in Scoops.

Dagger Scoop

Instead of adding individual services to your Scoops, we strongly recommend integration with Dagger. Replacing the vanilla ScreenScooper with DaggerScreenScooper will set up your application for dependency injection. DaggerScoop builds on top of Scoop, and is readily available for public use within the Scoop repository as an additional open source project.

Current state and future plans

In November of 2014, we first started development of Scoop. In December of 2015, we open sourced and released the framework for other developers to use. Our team is still heavily focused on building improvements and new features for Scoop, and we are always open to new feature requests or contributions.

Interested in open source work and having a big impact? Lyft is hiring! Drop me a note on LinkedIn or at pjohnson@lyft.com.

N E X T → Matchmaking in Lyft Line — Part 3