This document describes the architecture for the content suggestions UI. See the internal project page for more info about the project. This document covers the general principles and some aspects of the implementation, to be seen both as explanation of our solution and guidelines for future developments.
Make development easier. Code should be well-factored. Test coverage should be ubiquitous, and writing tests shouldn't be burdensome. Support for obsolete features should be easy to remove.
Allow for radical UI changes. The core architecture of the package should be structured to allow for flexibility and experimentation in the UI. This means it generally shouldn't be tied to any particular UI surface, and specifically that it is flexible enough to accomodate both the current NTP and its evolutions.
Decoupling. Components should not depend on other components explicitly. Where items interact, they should do so through interfaces or other abstractions that prevent tight coupling.
Encapsulation. A complement to decoupling is encapsulation. Components should expose little specifics about their internal state. Public APIs should be as small as possible. Architectural commonalities (for example, the use of a common interface for ViewHolders) will mean that the essential interfaces for complex components can be both small and common across many implementations. Overall the combination of decoupling and encapsulation means that components of the package can be rearranged or removed without impacting the others.
Separation of Layers. Components should operate at a specific layer in the adapter/view holder system, and their interactions with components in other layers should be well defined.
The UI is conceptually a list of views, and as such we are using the standard system component for rendering long and/or complex lists: the RecyclerView. It comes with a couple of classes that work together to provide and update data, display views and recycle them when they move out of the viewport.
Summary of how we use that pattern for suggestions:
RecyclerView: The list itself. It asks the Adapter for data for a given position, decides when to display it and when to reuse existing views to display new data. It receives user interactions, so behaviours such as swipe-to-dismiss or snap scrolling are implemented at the level of the RecyclerView.
Adapter: It holds the data and is the RecyclerView's feeding mechanism. For a given position requested by the RecyclerView, it returns the associated data, or creates ViewHolders for a given data type. Another responsibility of the Adapter is being a controller in the system by forwarding notifications between ViewHolders and the RecyclerView, requesting view updates, etc.
ViewHolder: They hold views and allow efficiently updating the data they display. There is one for each view created, and as views enter and exit the viewport, the RecyclerView requests them to update the view they hold for the data retrieved from the Adapter.
For more info, check out this tutorial that gives more explanations.
A specificity of our usage of this pattern is that our data is organised as a tree rather than as a flat list (see the next section for more info on that), so the Adapter also has the role of making that tree appear flat for the RecyclerView.
Build a tree of adapter-like nodes.
Making modification to the TreeNode:
As a result of this design, tree nodes can be added or removed depending on the current setup and the experiments enabled. Since nothing is hardcoded, only the initialisation changes. Nodes are specialised and are concerned only with their own functioning and don't need to care about their neighbours.
To make the package easily testable and coherent with our principles, interactions with the rest of Chrome goes through a set of interfaces. They are implemented by objects passed around during the object's creation. See their javadoc and the unit tests for more info.
SuggestionsUiDelegate
SuggestionsNavigationDelegate
SuggestionsMetrics
SuggestionsRanker
ContextMenuManager.Delegate
Context: A node is notified that it should be inserted. This is simply mixing the standard RecyclerView pattern usage from the system framework with our data tree.
Sample code path: SigninPromo.SigninObserver#onSignedOut()
Context: A node is notified that it needs to update some of the data that is already displayed. In this we also rely on the RecyclerView mechanism of partial updates that is supported in the framework, but our convention is to use callbacks as notification payload.
Sample code path: TileGrid#onTileOfflineBadgeVisibilityChanged()