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.
-
First, the underlying page view needs to display something, so it asks the
ComponentController
for a view to render. - 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 newComponentViewHolder
by callinggetHolderType
on the component in the list at the position that the page view needs. This method returns aComponentViewHolder
class which is then instantiated through reflection. - Since this is the first time the component is creating a view, the layout needs to be inflated. The
ComponentController
calls theinflate
method on the newly createdComponentViewHolder
to create the view. - Next, we need to populate the view with data and make sure it will react to user input. The
bind
method is called on theComponentViewHolder
instance that was created. This method is provided with a data item and a presenter. These are generated through theComponentController
calling thegetPresenter
andgetItem
methods of the correspondingComponent
. The presenter is any object that does some business logic or handles user interactions, In many cases it is theComponent
class itself. The data item is usually a data class with view properties and strings to display to the user. - 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 differentComponents
, 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 theComponent
is the Presenter, theComponentViewHolder
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 thetext
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 ourExampleViewHolder
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 intextView
so we can use it later when binding data. -
bind
– This method is called whenever an item in theComponent
is ready to be shown on screen. It is called once for each item as defined bygetCount
in theComponent
class. Thebind
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, thepresenter
argument is obtained from callinggetPresenter(i)
at some position i. Theelement
argument is also obtained from callinggetItem(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:
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 BentoInteraction
s 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.
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