一个用于分解 RecyclerView 布局以提高滚动性能的 Android 库。
Graywater is a RecyclerView adapter that facilitates the performant decomposition of complex and varied list items. It does this by mapping large data models to multiple viewholders, splitting the work needed to create a complex list item over multiple frames.
The concept is based off of Facebook's post on a faster news feed and Components for Android, which have been realized as Litho.
Tumblr developed Graywater to improve scroll performance, reduce memory usage, and lay the foundation for a more modular codebase.
The name "Graywater" comes from the process of recycling water.
An adapter basically takes a list of models (of type T) and maps them to a list of viewholders (of type VH extends RecyclerView.ViewHolder).
One naive solution is to map models directly to viewholders. For example, a list of "posts" can have a viewholder for each post. But this architecture quickly becomes slow and unwieldy if there is either a large variety of posts or if individual posts are complex.
So to improve performance, the parts of a post that are offscreen can be recycled.
model views
+---------+ +------+
| | | head | | Binder | --> | ViewHolder |
+-------+ +--------+ +------------+
We no longer desire the one-to-one relationship between models and viewholders that, because monolithic models result in monolithic viewholders. For example, a video post (VideoPost) used to have a corresponding VideoPostViewHolder. Instead, we want VideoPost to be composed of a header, body, and footer.
+--------+ +------------+
/--> | Binder | --> | ViewHolder |
+-------+ +---+ / +--------+ +------------+
| Model | --> | ? | *----> | Binder | --> | ViewHolder |
+-------+ +---+ \ +--------+ +------------+
\--> | Binder | --> | ViewHolder |
+--------+ +------------+
To manage this relationship, we introduce the concept of an ItemBinder, which aggregates the binders needed to display a post. It takes a model (T) and returns a list of binders, each of which bind the model to a specific viewholder.
+------------+
/-----> | ItemBinder |
/ +------------+
/ v
/ +--------+ +------------+
/ /----> | Binder | --> | ViewHolder |
+-------+ / +--------+ +------------+
| Model | *------> | Binder | --> | ViewHolder |
+-------+ \ +--------+ +------------+
\----> | Binder | --> | ViewHolder |
+--------+ +------------+
ItemBinder takes a model T and maps it to a list of binders of type Binder.Binder takes a model of type T and maps it to a ViewHolder of type VHA minor design point is that RecyclerView.Adapter#onCreate() creates the viewholders, so some sort of mechanism for creating viewholders is necessary. This is where ViewHolderCreator comes in - it is a model-independent way of creating viewholders (in other libraries with a one-to-one relationship between models and viewholders, this code would live in the model - e.g. Epoxy).
…
Binders ItemBinders Items Screen
+--------+ +------------+ +-----------+ +--------+
| Photo | -------- | | /- | TextPost | | Header |
+--------+ /---- | Photo Post | -\ / +-----------+ +--------+
| Footer | --x /--- | | ----- | PhotoPost | | |
+--------+ x +------------+ / +-----------+ | |
| Header | --x --- | | --/ /- | TextPost | | Text |
+--------+ ---- | Text Post | / +-----------+ | |
| Text | -------- | | ----/ | |
+--------+ +------------+ +--------+
But on-screen, only the first item is visible, and out of the first item, only two components are visible. So in the above example, there is no need to load the "Footer" binder. This is what `List>` facilitates.
Binders ItemBinders Items Screen
+--------+ +------------+ +-----------+ +--------+
| Photo | | | /-- | TextPost | -x--- | Header |
+--------+ | Photo Post | / +-----------+ \ +--------+
| Footer | | | / | PhotoPost | - | |
+--------+ +------------+ / +-----------+ | |
| Header | ---\ | | -/ | TextPost | | Text |
+--------+ --- | Text Post | +-----------+ | |
| Text | -------- | | | |
+--------+ +------------+ +--------+
This is very useful for improving initialization performance when loading long cached lists by deferring binder creation until the binder is nearly on screen.
## How do you use it?
Graywater relies heavily on generics for type safety - here are the major type parameters:
* `T` is the base model type.
* `VH` is the base viewholder type.
* `MT` is the type of the model type (e.g. `Class`).
Although this may seem overly generic, it is convenient if your base model or viewholder type has methods you need to access.
Add a model that subclasses `T`.
```java
class Text {
String text;
}
Create the viewholder(s).
class TextViewHolder extends RecyclerView.ViewHolder {
TextView textView;
public TextViewHolder(View view) {
super(view);
textView = (TextView) view.findViewById(R.id.text);
}
}
Create the corresponding ViewHolderCreator implementations.
class TextViewHolderCreator implements GraywaterAdapter.ViewHolderCreator {
public TextViewHolder create(final ViewGroup parent) {
return new TextViewHolder(GraywaterAdapter.inflate(parent, R.layout.item_text));
}
public int getViewType() {
return R.layout.item_text;
}
}
Create the Binder implementations for each ViewHolder.
class TextBinder implements GraywaterAdapter.Binder {
public Class getViewHolderType() {
return TextViewHolder.class;
}
public void prepare(final Text model,
final List> binders,
final int binderIndex) {
}
public void bind(final Text model,
final TextViewHolder holder,
final List> binders,
final int binderIndex,
final GraywaterAdapter.ActionListener actionListener) {
holder.textView.setText(model.text);
}
public void unbind(final TextViewHolder holder) {
holder.textView.setText(null);
}
}
Create the ItemBinder that returns the list of binders for the model.
class TextItemBinder implements GraywaterAdapter.ItemBinder {
TextBinder textBinder;
public TextItemBinder(TextBinder textBinder) {
this.textBinder = textBinder;
}
public List> getBinderList(
final Text model,
final int position) {
return new ArrayList>() {{
add(textBinder);
add(textBinder);
}};
}
}
Lastly, subclass GraywaterAdapter and register the created classes!
private static class TextAdapter extends GraywaterAdapter> {
public TextAdapter() {
register(new TextViewHolderCreator(), TextViewHolder.class);
final TextBinder textBinder = new TextBinder();
register(String.class, new TextItemBinder(textBinder), null);
}
@Override
protected Class getModelType(final Text model) {
return model.getClass();
}
}
…
java
protected abstract MT getModelType(T model);
Instead of automatically using the class of the model as the model's type, it can be anything (preferably a similar property of the model).
Note that RecyclerView.Adapter has these methods:
abstract class Adapter {
abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
abstract void onBindViewHolder(VH holder, int position);
int getItemViewType(int position);
abstract int getItemCount();
}
It is important to note that position in the above methods is the viewholder position, not the model position. This distinction is extremely important, because when we are given the viewholder position when we need the model position.
For now, we assume that viewType has a one-to-one correspondence to the viewholder class.
Here is a visualization of the model and viewholder positions:
model viewholder
position position
+-----+ +-----+
| | | 0 |
| | +-----+
| | | 1 |
| 0 | +-----+
| | | 2 |
| | +-----+
| | | 3 |
+-----+ +-----+
+-----+ +-----+
| | | 4 |
| 1 | +-----+
| | | 5 |
+-----+ +-----+
If we are given a viewholder position of 5, we need to arrive at the model position of 1, that way we can grab the model from the backing data store.
The way to do this is to iterate through the models, going to the corresponding ItemBinder and accumulating the size of the list that is returned. Unfortunately, this is slow.
But in order to make it fast, we need to cache a lot of intermediary state.
On add(), we compute these two caches, viewHolderToItemPosition and itemPositionToFirstViewHolderPosition. Note that the code uses item to refer to the model position.
model viewholder viewHolderToItemPos itemPosToFirstViewHolderPos
position position
+-----+ +-----+
| | | 0 | { 0, 0 }
| | +-----+
| | | 1 | { 1, 0 }
| 0 | +-----+ { 0, 0 }
| | | 2 | { 2, 0 }
| | +-----+
| | | 3 | { 3, 0 }
+-----+ +-----+
+-----+ +-----+
| | | 4 | { 4, 1 }
| 1 | +-----+ { 1, 4 }
| | | 5 | { 5, 1 }
+-----+ +-----+
viewHolderToItemPositionCache is also used for getItemCount().
itemPositionToFirstViewHolderPosition is primarily used for one purpose: to determine the position of the viewholder and associated binder in the list of viewholders for a given model. In the above example, the viewholder at position 5 is the 2nd viewholder for the 2nd model. This is important when there is more than one instance of a viewholder for a model, such as reblog comments.
getItemViewType() works by tracking the registered ViewHolderCreators, which have this interface:
interface ViewHolderCreator {
RecyclerView.ViewHolder create(ViewGroup parent);
int getViewType();
}
When a new ViewHolderCreator is registered, it is added to viewHolderCreatorList, which is of type SparseArray>, and associates the viewType with the correct class. The class is then associated with the ViewHolderCreator via viewHolderCreatorMap, which is of type Map, ViewHolderCreator>.
viewHolderCreatorList viewHolderCreatorMap
+------------------------------+
暂无开放 Issues,或尚未同步最近议题。