# Content Suggestions UI: Architecture and Package Overview
## Introduction
This document describes the architecture for the content suggestions UI. See the
[internal project page](https://goto.google.com/chrome-content-suggestions) 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.
## Goals
- **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.
## Principles
- **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.
## Core Anatomy
### The RecyclerView / Adapter / ViewHolder pattern
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][rv_doc]. 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][detailed 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.
[rv_doc]: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html
[detailed tutorial]: http://willowtreeapps.com/ideas/android-fundamentals-working-with-the-recyclerview-adapter-and-viewholder-pattern/
### Representation of the data: the node tree
#### Problem
- RecyclerView.Adapter exposes items as a single list.
- The Cards UI has nested structure: the UI has a list of card sections, each
section has a list of cards, etc.
- There are dependencies between nearby items: e.g. a status card is shown if
the list of suggestion cards is empty.
- We want to avoid tight coupling: A single adapter coupling the logic for
different UI components together, a list of items coupling the model
(SnippetArticle) to the controller, etc.
- Triggering model changes in parts of the UI is complicated, since item
offsets need to be adjusted globally.
#### Solution
Build a tree of adapter-like nodes.
- Each node represents any number of items:
* A single node can represent a homogenous list of items.
* An "optional" node can represent zero or one item (allowing toggling its
visibility).
- Inner nodes dispatch methods to their children.
- Child nodes notify their parent about model changes. Offsets can be adjusted
while bubbling changes up the hierarchy.
- Polymorphism allows each node to represent / manage its own items however it
wants.
Making modification to the TreeNode:
- ChildNode silently swallows notifications before its parent is assigned.
This allows constructing tree or parts thereof without sending spurious
notifications during adapter initialization.
- Attaching a child to a node sets its parent and notifies about the number of
items inserted.
- Detaching a child notifies about the number of items removed and clears the
parent.
- The number of items is cached and updated when notifications are sent to the
parent, meaning that a node is _required_ to send notifications any time its
number of items changes.
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.
### Interactions with the rest of Chrome
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`](SuggestionsUiDelegate.java)
- [`SuggestionsNavigationDelegate`](SuggestionsNavigationDelegate.java)
- [`SuggestionsMetrics`](SuggestionsMetrics.java)
- [`SuggestionsRanker`](SuggestionsRanker.java)
- [`ContextMenuManager.Delegate`](../ntp/ContextMenuManager.java)
## Appendix
### Sample operations
#### 1. Inserting an item
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()`][cs_link_1]
- A Node wants to insert a new child item.
- The Node notifies its parent of the range of indices to be inserted
- Parent maps the range of indices received from the node to is own range and
propagates the notification upwards, repeating this until it reaches the root
node, which is the Adapter.
- The Adapter notifies the RecyclerView that it has new data about a range of
positions where items should be inserted.
- The RecyclerView requests from the Adapter the view type of the data at that
position.
- The Adapter propagates the request down the tree, the leaf for that position
eventually returns a value
- If the RecyclerView does not already have a ViewHolder eligible to be recycled
for the returned type, it asks the Adapter to create a new one.
- The RecyclerView asks the Adapter to bind the data at the considered position
to the ViewHolder it allocated for it.
- The Adapter transfers the ViewHolder down the tree to the leaf associated to
that position
- The leaf node updates the view holder with the data to be displayed.
- The RecyclerView performs the associated canned animation, attaches the view
and displays it.
[cs_link_1]: https://cs.chromium.org/chromium/src/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SignInPromo.java?l=174&rcl=da4b23b1d2a82705f7f4fdfb6c9c8de00341c0af
#### 2. Modifying an existing item
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()`][cs_link_2]
- A Node wants to update the view associated to a currently bound item.
- The Node notifies its parent that a change happened at a specific position,
using a callback as payload.
- The notification bubbles up to the Adapter, which notifies the RecyclerView.
- The RecyclerView calls back to the Adapter with the ViewHolder to modify and
the payload it received.
- The Adapter runs the callback, passing the ViewHolder as argument.
[cs_link_2]: https://cs.chromium.org/chromium/src/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGrid.java?l=78&rcl=da4b23b1d2a82705f7f4fdfb6c9c8de00341c0af