Bento – An Android UI Framework

https://engineeringblog.yelp.com/2019/05/introducing-bento.html

Bento Logo

Today we’re proud to introduce Bento, an open source framework for building modularized Android user interfaces, created here at Yelp. Over the past year, we’ve seen great developer productivity gains and product design flexibility from using Bento on our most critical screens. In this post we’ll explain a bit about how Bento works, why you might want to use it, and where we want to go next.

What is Bento?

We named this framework after the wonderfully compartmentalized Japanese lunch container. A Bento box is a container with dividers to separate different food items from each other. If you squint really hard and maybe take a step back, it also looks like the way most apps are designed these days: a list of colorful components arranged in a somewhat staggered grid.

Many Android apps that have a list-based user interface use a RecyclerView to display their views. At a basic level, the RecyclerView works by referencing an ordered list of data and creating a view on screen for each item of data in that list. That works really well if your list consists of homogenous data types, but can quickly become unruly when you need to manage an unbounded number of data and view types in a list. It also becomes an issue if you need to use the same view type in a user interface other than a RecyclerView, such as a ViewPager or ListView.

Bento aims to fix these issues by providing a framework to manage the complexity of handling different view types and the dynamic position of each view in the list. Bento can also be used to manage views in other parent view types, such as ViewPagers and ListViews, all while keeping the benefits of RecyclerView best practices (like view holders and view recycling).

How Does Bento Work?

Bento groups different view types and the logic associated with displaying and interacting with those view types into “Components”. A Component can be anything from a simple text view to a horizontal carousel comprised of other components.

At its core, a Component is a self-contained element that provides a data item. An associated ComponentViewHolder class will inflate a view and bind the data item provided to the inflated view. The view holder will also typically bind the Component (or some Presenter) to the view to handle any user interactions.

To demonstrate how a Component works, here’s a diagram of the data flow of a component to be displayed on screen.

Bento Data Flow

  1. First, the underlying page view needs to display something, so it asks the ComponentController for a view to render.

  2. The ComponentController needs to return an updated view to the underlying page view;so based on the internal list of components the controller maintains, it creates a new ComponentViewHolder by calling getHolderType on the component in the list at the position that the page view needs. This method returns a ComponentViewHolder class which is then instantiated through reflection.
  3. Since this is the first time the component is creating a view, the layout needs to be inflated. The ComponentController calls the inflate method on the newly created ComponentViewHolder to create the view.
  4. Next, we need to populate the view with data and make sure it will react to user input. The bind method is called on the ComponentViewHolder instance that was created. This method is provided with a data item and a presenter. These are generated through the ComponentController calling the getPresenter and getItem methods of the corresponding Component. The presenter is any object that does some business logic or handles user interactions, In many cases it is the Component class itself. The data item is usually a data class with view properties and strings to display to the user.
  5. The view is updated with the data item and event listeners are bound to the presenter. The view is then passed back to the underlying page view to be rendered.

The order of a Component in its parent view relative to other components is determined by the ComponentController. This interface is the magic soy sauce in our Bento box that allows us to add, remove, and insert components dynamically into the ordering as if we were manipulating values in a simple list data structure. It also provides an abstraction we can use to apply this functionality to different view types, such as RecyclerView, ListView, ViewPager, and potentially many others. For example, the RecyclerViewComponentController handles the complex choreography of communicating with the RecyclerView class and adapter to determine spans and positions, making it very simple to manage diverse sets of components in a list. We can also create groupings of different components using a ComponentGroup, which is also a Component itself, to keep logical groupings of components together in the list.

The Bento framework makes it easy to break down complex interfaces into a set of easy to understand, modular, dynamic, and testable components.

An Example

Let’s take a look at an example of how to build a very basic component that just renders some text. Here’s an example of a very simple Component class:

class ExampleComponent(private val text: String): Component() {
    override fun getCount() = 1

    override fun getPresenter(position: Int) = this

    override fun getItem(position: Int) = text

    override fun getHolderType(position: Int) = ExampleViewHolder::class.java
}

Here we can see we’ve overridden some methods of the abstract Component class. Let’s take a look at each one:

  • getCount – Components can be internally made up of a series of items. In our simple case, we only have one item. Each item in the component at each position can have its own presenter, data item, and view holder type if we wanted, but it’s usually best to break it into different Components, unless all items have an identical view holder and presenter.
  • getPresenter – The presenter is the brains of the component that knows how to respond to user interactions and do other complex state-driven things. In a way, each Bento component is it’s own MVP ecosystem where the Component is the Presenter, the ComponentViewHolder is the View, and the data item is the Model.
  • getItem – The item is the data that’s associated with the component at the specified position. In this case, our data is the text that we want to display.
  • getHolderType – The holder type is a class that is instantiated by the Bento framework through reflection. It’s responsible for inflating the component’s layout and binding the data item to the view. Let’s take a look at our ExampleViewHolder class:
class ExampleViewHolder: ComponentViewHolder<ExampleComponent, String>() {

    private lateinit var textView: TextView

    override fun inflate(parent: ViewGroup) =
            parent.inflate<TextView>(R.layout.example_component_layout)
            .also { textView = it }

    override fun bind(presenter: ExampleComponent, element: String) {
        textView.text = element
    }
}

Much like the view holder pattern we see when using RecyclerViews, Bento’s view holders are separated into an inflate and a bind method. Let’s take a look at what these methods are doing:

  • inflate – Here we inflate a layout file which, at its root, contains nothing but a simple TextView element. We then return that inflated view and store a reference to it in textView so we can use it later when binding data.
  • bind – This method is called whenever an item in the Component is ready to be shown on screen. It is called once for each item as defined by getCount in the Component class. The bind method provides a reference to a presenter and the corresponding data item at the position in the component that this view holder represents. In other words, the presenter argument is obtained from calling getPresenter(i) at some position i. The element argument is also obtained from calling getItem(i) for the same position i.

  • NOTE: The bind method is often called as views are recycled in the list, so performance should be a high priority for this method.

Great! So now we have a Component and a ComponentViewHolder that will take some string and bind it to a TextView to show the user. So how do we actually use the component? We need to create a ComponentController that organizes all of the components. For this example, we’ll use the simple RecyclerViewComponentController. Here it is in an example activity:

class ExampleActivity: AppCompatActivity() {

    private val componentController by lazy {
        RecyclerViewComponentController(recyclerView)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recycler_view)
        componentController.addComponent(ExampleComponent("Hello World!"))
    }
}

Here we create a regular activity whose content view layout is just a RecyclerView with an id of recyclerView. We lazily initialize the ComponentController the first time it is referenced, and create it by passing in the instance of the RecyclerView. From there, we call addComponent and pass in a new instance of our ExampleComponent with a text string of Hello World to display. Here’s what the app looks like when rendered:

Screenshot of Bento Hello World

Nice! Bento also has helper classes to avoid boilerplate code. Our Example component class was actually pretty simple, and so we can write it as a SimpleComponent:

class SimpleExampleComponent(
    private val text: String
): SimpleComponent<Nothing>(ExampleViewHolder::class.java) {
    override fun getItem(position: Int) = text
}

It’s still using our ExampleViewHolder from before, but now we don’t need to worry about the count or the presenter, and the view holder type is specified in the super constructor. There are also many variations of components included with Bento, including a ListComponent for repeating views, a PaginatingListComponent for lazy loading, and even a CarouselComponent for collections of components that can be scrolled horizontally.

Features

Modular

As you can tell from the above example, Components are pretty modular. The component exists as a cohesive whole without any dependencies on the environment into which it’s inserted. That’s nice for a lot of reasons, most of which are covered in this section.

Testable

Since Components don’t rely on the Android framework, it’s very easy to unit test their logic. It’s also easy to test the logic of a Component view holder using the ComponentViewHolderTestCase that inflates the view and injects it into a testing activity where the data is bound. Then, Espresso can check that everything is displayed properly and the correct methods are called on the presenter during user interactions. From an integration testing standpoint, the bento-testing module provides some BentoInteractions to test an entire screen of components as the user would see it.

Reusable

A component created for one environment can be reused across many different screens that are using a ComponentController of any kind. That means it’s easy to drop the same component into a RecyclerView, ListView, or ViewPager.

Progressive

Bento was made to be progressively introduced into an existing application. You don’t need to rewrite your app from scratch or rethink your entire application architecture. We’ve been integrating it into the Yelp consumer and business owner apps for almost a year now. For example, on the nearby screen of the consumer app, everything below the header (outlined below in red) is a Bento component.

Screenshot of Bento Components

We’ve also incorporated some tools to make the transition easier for existing apps. For example, for those of you still stuck using ListViews (this is a judgement-free zone), you can use the ListAdapterComponent to wrap your existing list into its own component, and start converting the items in the list into their own separate components.

Scalable

Bento is scalable from a technical and organizational standpoint. There’s no limit to the number of components a project using Bento can have. Because it’s easy to keep components isolated from one another, it’s also easy to separate them across modules for faster build times. Since we can use RecyclerViews as a backing view for Bento, it’s also very performant when using a large number of heterogeneous components. More modular user interface components also mean that we can assign ownership of a component to a particular team for maintenance. Instead of one screen being one team’s problem, now other teams can own components on that screen, meaning the weight of software maintenance can be more evenly distributed and bugs more easily triaged.

Low Overhead

Bento doesn’t use type annotations. That means there’s a very low compile-time overhead since no annotation processing needs to happen. Also, since Bento is only a handful of classes, it has a very small storage footprint. The aar library file is only 105.6 KB.

Where to Next?

Bento has helped our Android app development scale and allowed us to execute new features efficiently and reliably. But Bento is still growing, and is by no means perfect or complete. We have several areas which we’d like to improve, mostly centered around performance. We currently don’t have asynchronous layout inflation, where each layout is inflated off of the main UI thread. We also don’t have automatic diffing when notifying of a change in a ComponentGroup. Parts of the framework are still written in Java while others are written in Kotlin. That being said, we’re always looking for new contributors to the project! If you’d like to contribute to the project, please check out the repo on GitHub at github.com/Yelp/bento and follow the contributing steps in the readme.

Want to build next-generation Android application infrastructure?

We’re hiring! Become an Android developer at Yelp

View Job

Back to blog

Leave a Reply

Your email address will not be published. Required fields are marked *

Next Post

In this Twitter exchange, jetBlue explains to a passenger how it got a photo of her face -- from the DHS

Fri May 3 , 2019
https://boingboing.net/2019/04/23/in-this-twitter-exchange-jetb.html