Epoxy is an Android library for building complex screens in a RecyclerView
https://github.com/airbnb/epoxy.git
Epoxy is an Android library for building complex screens in a RecyclerView. Models are automatically generated from custom views or databinding layouts via annotation processing. These models are then used in an EpoxyController to declare what items to show in the RecyclerView.
This abstracts the boilerplate of view holders, diffing items and binding payload changes, item types, item ids, span counts, and more, in order to simplify building screens with multiple view types. Additionally, Epoxy adds support for saving view state and automatic diffing of item changes.
We developed Epoxy at Airbnb to simplify the process of working with RecyclerViews, and to add the missing functionality we needed. We now use Epoxy for most of the main screens in our app and it has improved our developer experience greatly.
Gradle is the only supported build configuration, so just add the dependency to your project build.gradle file:
dependencies {
implementation "com.airbnb.android:epoxy:$epoxyVersion"
// Add the annotation processor if you are using Epoxy's annotations (recommended)
annotationProcessor "com.airbnb.android:epoxy-processor:$epoxyVersion"
}
Replace the variable $epoxyVersion with the latest version :
See the releases page for up to date release versions and details
apply plugin: 'kotlin-kapt'
kapt {
correctErrorTypes = true
}
so that AutoModel annotations work properly. More information here
Also, make sure to use kapt instead of annotationProcessor in your dependencies in the build.gradle file.
Add the KSP plugin to your root build.gradle:
plugins {
id 'com.google.devtools.ksp' version "$KSP_VERSION" apply false
}
Then apply it in your module's build.gradle:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.devtools.ksp'
}
dependencies {
implementation "com.airbnb.android:epoxy:$epoxyVersion"
ksp "com.airbnb.android:epoxy-processor:$epoxyVersion"
}
You can configure KSP processor options:
ksp {
// Validation and debugging
arg("validateEpoxyModelUsage", "true") // Validate model usage at runtime (default: true)
arg("logEpoxyTimings", "false") // Log annotation processing timings (default: false)
// Code generation options
arg("epoxyDisableGenerateReset", "false") // Disable reset() method generation (default: false)
arg("epoxyDisableGenerateGetters", "false") // Disable getter generation (default: false)
arg("epoxyDisableGenerateOverloads", "false") // Disable builder overload generation (default: false)
arg("disableEpoxyKotlinExtensionGeneration", "false") // Disable Kotlin extension generation (default: false)
arg("epoxyDisableDslMarker", "false") // Disable DSL marker annotation (default: false)
// Model requirements
arg("requireHashCodeInEpoxyModels", "false") // Require hashCode/equals in models (default: false)
arg("requireAbstractEpoxyModels", "false") // Require abstract model classes (default: false)
arg("implicitlyAddAutoModels", "false") // Auto-add models to controllers (default: false)
}
Important: DataBinding models are not supported with KSP, as Android's DataBinding library itself uses KAPT. If you need DataBinding support, you must continue using KAPT. For custom views with @ModelView or ViewHolder models, KSP works perfectly.
See the epoxy-kspsample module for a complete working example.
buildscript.
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:10.1.0'
}
}
and then apply it in your module:
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
Now make sure you use R2 instead of R inside all Epoxy annotations.
@ModelView(defaultLayout = R2.layout.view_holder_header)
public class HeaderView extends LinearLayout {
....
}
This is not necessary if you don't use resources as annotation parameters, such as with custom view models.
EpoxyModels that describe how your views should be displayed in the RecyclerView.EpoxyController where the models are used to describe what items to show and with what data._) are used directly in your EpoxyController classes.
@ModelView annotation on a view class. Then, add a "prop" annotation on each setter method to mark it as a property for the model.
@ModelView(autoLayout = Size.MATCH_WIDTH_WRAP_HEIGHT)
public class HeaderView extends LinearLayout {
... // Initialization omitted
@TextProp
public void setTitle(CharSequence text) {
titleView.setText(text);
}
}
A HeaderViewModel_ is then generated in the same package.
If you use Android DataBinding you can simply set up your xml layouts like normal:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="title" type="String" />
</data>
<TextView
android:layout_width="120dp"
android:layout_height="40dp"
android:text="@{title}" />
</layout>
Then, create an interface or class in any package and add an EpoxyDataBindingLayouts annotation to declare your databinding layouts.
package com.airbnb.epoxy.sample;
import com.airbnb.epoxy.EpoxyDataBindingLayouts;
@EpoxyDataBindingLayouts({R.layout.header_view, ... // other layouts })
interface EpoxyConfig {}
From this layout name Epoxy generates a HeaderViewBindingModel_.
@EpoxyModelClass(layout = R.layout.header_view)
public abstract class HeaderModel extends EpoxyModelWithHolder<Holder> {
@EpoxyAttribute String title;
@Override
public void bind(Holder holder) {
holder.header.setText(title);
}
static class Holder extends BaseEpoxyHolder {
@BindView(R.id.text) TextView header;
}
}
A HeaderModel_ class is generated that subclasses HeaderModel and implements the model details.
A controller defines what items should be shown in the RecyclerView, by adding the corresponding models in the desired order.
The controller's buildModels method declares which items to show. You are responsible for calling requestModelBuild whenever your data changes, which triggers buildModels to run again. Epoxy tracks changes in the models and automatically binds and updates views.
As an example, our PhotoController shows a header, a list of photos, and a loader (if more photos are being loaded). The controller's setData(photos, loadingMore) method is called whenever photos are loaded, which triggers a call to buildModels so models representing the state of the new data can be built.
public class PhotoController extends Typed2EpoxyController<List<Photo>, Boolean> {
@AutoModel HeaderModel_ headerModel;
@AutoModel LoaderModel_ loaderModel;
@Override
protected void buildModels(List<Photo> photos, Boolean loadingMore) {
headerModel
.title("My Photos")
.description("My album description!")
.addTo(this);
for (Photo photo : photos) {
new PhotoModel()
.id(photo.id())
.url(photo.url())
.addTo(this);
}
loaderModel
.addIf(loadingMore, this);
}
}
class PhotoController : Typed2EpoxyController<List<Photo>, Boolean>() {
override fun buildModels(photos: List<Photo>, loadingMore: Boolean) {
header {
id("header")
title("My Photos")
description("My album description!")
}
photos.forEach {
photoView {
id(it.id())
url(it.url())
}
}
if (loadingMore) loaderView { id("loader") }
}
}
Get the backing adapter off the EpoxyController to set up your RecyclerView:
MyController controller = new MyController();
recyclerView.setAdapter(controller.getAdapter());
// Request a model build whenever your data changes
controller.requestModelBuild();
// Or if you are using a TypedEpoxyController
controller.setData(myData);
If you are using the EpoxyRecyclerView integration is easier.
epoxyRecyclerView.setControllerAndBuildModels(new MyController());
// Request a model build on the recyclerview when data changes
epoxyRecyclerView.requestModelBuild();
epoxyRecyclerView.withModels {
header {
id("header")
title("My Photos")
description("My album description!")
}
photos.forEach {
photoView {
id(it.id())
url(it.url())
}
}
if (loadingMore) loaderView { id("loader") }
}
}
Epoxy handles much more than these basics, and is highly configurable. See the wiki for in depth documentation.
If you still have questions, feel free to create a new issue.
Copyright 2016 Airbnb, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.