Skip to content

Commit

Permalink
Prepare 2.2.0 release (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
elihart authored Jun 19, 2017
1 parent 6d4cd3b commit d2e2989
Show file tree
Hide file tree
Showing 27 changed files with 260 additions and 180 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 2.2.0 (June 19, 2017)
* **Main Feature** Models can now be completely generated from a custom view via annotations on the view. This should completely remove the overhead of creating a model manually in many cases! For more info, see [the wiki](https://github.com/airbnb/epoxy/wiki/Generating-Models-from-View-Annotations)

* **New** Lowered the minimum SDK from 16 to 14.
* **New** Models that have a `View.OnLongClickListener` as an EpoxyAttribute will now have an overloaded setter on the generated model that allows you to set a long click listener that will return the model, view, and adapter position. This is very similar to the `View.OnClickListener` support added in 2.0.0, but for long click listeners. **Upgrade Note** If you were setting a long click listener value to null anywhere you will need to now cast that to `View.OnLongClickListener` because of the new overloaded method.
* **New** `id` overload on EpoxyModel to define a model id with multiple strings
* **New** Option in `EpoxyAttribute` to not include the attribute in the generated `toString` method (Thanks to @geralt-encore!)
* **New** @AutoModel models are now inherited from usages in super classes (Thanks to @geralt-encore!)
* **Fixed** Generated getters could recursively call themselves (Thanks to @geralt-encore!)

# 2.1.0 (May 9, 2017)

* **New**: Support for Android Data Binding! Epoxy will now generate an EpoxyModel directly from a Data Binding xml layout, and handle all data binding details automatically. Thanks to @geralt-encore for helping with this! See more details in [the wiki](https://github.com/airbnb/epoxy/wiki/Data-Binding-Support).
Expand Down
129 changes: 125 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Epoxy

Epoxy is an Android library for building complex screens in a RecyclerView. It abstracts the boilerplate of view holders, 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.
Epoxy is an Android library for building complex screens in a RecyclerView. Models are automatically generated from custom views, databinding layouts, or Litho components 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, computing item diffs, 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](https://medium.com/airbnb-engineering/epoxy-airbnbs-view-architecture-on-android-c3e1af150394#.xv4ymrtmk) 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.

* [Installation](#installation)
* [Basic Usage](#basic-usage)
* [Documentation](#documentation)
* [Min SDK](#min-sdk)
* [Contributing](#contributing)
* [Sample App](/epoxy-sample)

Expand All @@ -15,23 +19,140 @@ Gradle is the only supported build configuration, so just add the dependency to

```groovy
dependencies {
compile 'com.airbnb.android:epoxy:2.1.0'
compile 'com.airbnb.android:epoxy:2.2.0'
}
```

Optionally, if you want to [take advantage of generated models](https://github.com/airbnb/epoxy/wiki/Epoxy-Models#annotations) you must also provide the annotation processor as a dependency.
```groovy
dependencies {
compile 'com.airbnb.android:epoxy:2.1.0'
annotationProcessor 'com.airbnb.android:epoxy-processor:2.1.0'
compile 'com.airbnb.android:epoxy:2.2.0'
annotationProcessor 'com.airbnb.android:epoxy-processor:2.2.0'
}
```

## Basic Usage
There are two main components of Epoxy:

1. The `EpoxyModel`s that describe how your views should be displayed in the RecyclerView.
2. The `EpoxyController` where the models are used to describe what items to show and with what data.

### Creating Models
There are a few ways to create models, depending on whether you prefer to use custom views, databinding, or other approaches.

#### From Custom Views
You can easily generate an EpoxyModel from your custom views by using the `@ViewModel` annotation on the class. Then, add a `ModelProp` annotation on each setter method to mark it as a property for the model.

```java
@ModelView(defaultLayout = R.layout.view_holder_header)
public class HeaderView extends LinearLayout {

... // Initialization omitted

@ModelProp(options = Option.GenerateStringOverloads)
public void setTitle(CharSequence text) {
titleView.setText(text);
}

@ModelProp(options = Option.GenerateStringOverloads)
public void setDescription(CharSequence text) {
captionView.setText(text);
}
}
```

The layout file (`R.layout.view_holder_header` in this case) simply describes the layout params and styling for how the view should be inflated.
```xml
<?xml version="1.0" encoding="utf-8"?>
<com.airbnb.epoxy.sample.views.HeaderView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="120dp" />
```

#### From DataBinding

If you use Android DataBinding you can simply set up your xml layouts like normal:

```xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<variable
name="url"
type="String" />

</data>

<Button
android:layout_width="120dp"
android:layout_height="40dp"
android:imageUrl="@{url}" />
</layout>
```

Then, create a `package-info.java` class in the package you want the models generated and add an `EpoxyDataBindingLayouts` annotation to declare all of the databinding layouts that should be used to create models.

```java
@EpoxyDataBindingLayouts({R.layout.photo, ... // other layouts })
package com.airbnb.epoxy.sample;

import com.airbnb.epoxy.EpoxyDataBindingLayouts;
import com.airbnb.epoxy.R;
```

Epoxy generates a model that includes all the variables for that layout.

#### Other ways
You can also create EpoxyModel's from Litho components, viewholders, or completely manually. See the wiki sidebar for more information on these approaches in depth.
### Using your models in a controller
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 the current view, and is called whenever the data backing the view changes. 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.
```java
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);
}
}
```
And that's it! The controller's declarative style makes it very easy to visualize what the RecyclerView will look like, even when many different view types or items are used. Epoxy handles everything else. If a view only partially changes, such as the description, only that new value is set on the view, so the system is very efficient
Epoxy handles much more than these basics, and is highly configurable. See the wiki for in depth documentation.
## Documentation
See examples and browse complete documentation at the [Epoxy Wiki](https://github.com/airbnb/epoxy/wiki)
If you still have questions, feel free to create a new issue.
## Min SDK
We support a minimum SDK of 14. However, Epoxy is based on the v7 support libraries so it should work with lower versions if you care to override the min sdk level in the manifest.
If you are using the [optional Litho integration](https://github.com/airbnb/epoxy/wiki/Litho-Support) then the min SDK is 15 due to Litho's SDK requirement.

## Contributing
Pull requests are welcome! We'd love help improving this library. Feel free to browse through open issues to look for things that need work. If you have a feature request or bug, please open a new issue so we can track it.
Expand Down
3 changes: 2 additions & 1 deletion blessedDeps.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ rootProject.ext.JAVA_TARGET_VERSION = JavaVersion.VERSION_1_7

rootProject.ext.TARGET_SDK_VERSION = 25
rootProject.ext.COMPILE_SDK_VERSION = 25
rootProject.ext.MIN_SDK_VERSION = 16
rootProject.ext.MIN_SDK_VERSION = 14
rootProject.ext.MIN_SDK_VERSION_LITHO = 15

rootProject.ext.ANDROID_BUILD_TOOLS_VERSION = "25.0.2"
rootProject.ext.ANDROID_SUPPORT_LIBS_VERSION = "25.3.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.LongSparseArray;
import android.support.v4.util.LongSparseArray;

import java.util.List;

Expand Down
2 changes: 2 additions & 0 deletions epoxy-databinding/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ android {
}

dataBinding {
// DataBinding is pulling in an old version of the support library which makes lint complain.
// Ignoring it for now until we can figure out how to fix it.
enabled = true
}
}
Expand Down
2 changes: 1 addition & 1 deletion epoxy-litho/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ android {
buildToolsVersion rootProject.ANDROID_BUILD_TOOLS_VERSION

defaultConfig {
minSdkVersion rootProject.MIN_SDK_VERSION
minSdkVersion rootProject.MIN_SDK_VERSION_LITHO
targetSdkVersion rootProject.TARGET_SDK_VERSION
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ConfigManager {
static final String PROCESSOR_OPTION_VALIDATE_MODEL_USAGE = "validateEpoxyModelUsage";
static final String PROCESSOR_OPTION_REQUIRE_HASHCODE = "requireHashCodeInEpoxyModels";
static final String PROCESSOR_OPTION_REQUIRE_ABSTRACT_MODELS = "requireAbstractEpoxyModels";
static final String PROCESSOR_IMPLICITLY_ADD_AUTO_MODELS = "implicitlyAddAutoModels";
static final String PROCESSOR_OPTION_IMPLICITLY_ADD_AUTO_MODELS = "implicitlyAddAutoModels";

private static final PackageConfigSettings
DEFAULT_PACKAGE_CONFIG_SETTINGS = PackageConfigSettings.forDefaults();
Expand All @@ -51,7 +51,7 @@ class ConfigManager {
PackageEpoxyConfig.REQUIRE_ABSTRACT_MODELS_DEFAULT);

globalImplicitlyAddAutoModels =
getBooleanOption(options, PROCESSOR_IMPLICITLY_ADD_AUTO_MODELS,
getBooleanOption(options, PROCESSOR_OPTION_IMPLICITLY_ADD_AUTO_MODELS,
PackageEpoxyConfig.IMPLICITLY_ADD_AUTO_MODELS_DEFAULT);
this.typeUtils = typeUtils;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,21 @@ void process(RoundEnvironment roundEnv) {
}
}

/**
* True if controller classes have been parsed and their java classes need to be written. We need
* to wait for other models to finish being generated first so we can resolve generated model
* references.
*
* @see #resolveGeneratedModelsAndWriteJava(List)
*/
boolean hasControllersToGenerate() {
return !controllerClassMap.isEmpty();
}

void resolveGeneratedModelsAndWriteJava(List<GeneratedModelInfo> generatedModels) {
resolveGeneratedModelNames(controllerClassMap, generatedModels);
generateJava(controllerClassMap);
controllerClassMap.clear();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.airbnb.epoxy;

import android.support.annotation.Nullable;

import com.squareup.javapoet.ClassName;

import javax.lang.model.element.Element;
Expand Down Expand Up @@ -44,10 +46,14 @@ class DataBindingModelInfo extends GeneratedModelInfo {
collectMethodsReturningClassType(superClassElement, typeUtils);
}

/**
* Look up the DataBinding class generated for this model's layout file and parse the attributes
* for it.
*/
void parseDataBindingClass() {
// This databinding class won't exist until the second round of annotation processing since
// it is generated in the first round.
Element dataBindingClass = getElementByName(dataBindingClassName, elementUtils, typeUtils);
Element dataBindingClass = getDataBindingClassElement();

HashCodeValidator hashCodeValidator = new HashCodeValidator(typeUtils, elementUtils);
for (Element element : dataBindingClass.getEnclosedElements()) {
Expand All @@ -58,6 +64,11 @@ void parseDataBindingClass() {
}
}

@Nullable
Element getDataBindingClassElement() {
return getElementByName(dataBindingClassName, elementUtils, typeUtils);
}

private ClassName getDataBindingClassNameForResource(LayoutResource layoutResource,
String moduleName) {
StringBuilder builder = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,49 @@ void process(RoundEnvironment roundEnv) {
}
}

/**
* True if databinding models have been parsed and are waiting for the DataBinding classes to be
* generated before they can be written.
*/
boolean hasModelsToWrite() {
return !modelInfoList.isEmpty();
}

boolean isDataBindingClassesGenerated() {
for (DataBindingModelInfo modelInfo : modelInfoList) {
// This should be called in the last round of processing so the data binding classes will
// have been generated by now and we can parse them to get the databinding variables
Element dataBindingClassElement = modelInfo.getDataBindingClassElement();
if (dataBindingClassElement == null) {
return false;
}
}

return true;
}

List<DataBindingModelInfo> resolveDataBindingClassesAndWriteJava() {
for (DataBindingModelInfo modelInfo : modelInfoList) {
Element dataBindingClassElement = modelInfo.getDataBindingClassElement();
if (dataBindingClassElement == null) {
errorLogger.logError(
"Unable to find databinding class for layout %s, so an Epoxy model could not be "
+ "generated.",
modelInfo.getLayoutResource().resourceName);
continue;
}

// This should be called in the last round of processing so the data binding classes will
// have been generated by now and we can parse them to get the databinding variables
modelInfo.parseDataBindingClass();
}

writeJava();
return modelInfoList;

ArrayList<DataBindingModelInfo> result = new ArrayList<>(modelInfoList);
modelInfoList.clear();

return result;
}

private void writeJava() {
Expand Down
Loading

0 comments on commit d2e2989

Please sign in to comment.