================================================
FILE: CHANGELOG.md
================================================
# 5.2.1
Fix some issues in KSP2 when multiple processing rounds occur (https://github.com/airbnb/epoxy/pull/1396)
# 5.2.0
Migrate to KSP 2 (#1393)
# 5.1.4
Change the way the Compose interop works to avoid Android 12 bug (#1370)
# 5.1.3
Update to kotlin 1.8.21
Fix click listener kapt bug (#1327)
Resolve unchecked call warning for WrappedEpoxyModelClickListener (#1337)
Fix refresh KDoc (#1334)
epoxy-kspsample : use ksp block to specify arguments (#1347)
# 5.1.2
Updates kotlin, ksp, and the xprocessing library.
Notably, the androidx.room:room-compiler-processing library (aka xprocessing) has been updated to 2.6.0-alpha01. This version is incompatible with previous versions due to a breaking API change. All annotation processors using this library must be on the same version. Other annotation processors such as Epoxy and Paris also use xprocessing and if you use them you need to use a version of them that also uses xprocessing 2.6.0-alpha01
# 5.1.1
Remove incorrect ksp symbol validation in processing of @EpoxyModelClass
# 5.1.0
Updates Kotlin to 1.7.20 and KSP to 1.7.20-1.0.7, as well as the room compiler processing (xprocessing) library to 2.5.0-beta01.
Also deletes the epoxy-paging artifact in favor of the newer epoxy-paging3
# 5.0.0
This adds support for Kotlin Symbol Processing, while maintaining backwards compatibility with java annotation processing via the xprocessing library from Room.
This includes a major version bump to 5.0.0 because there may be slight behavior differences with KSP, especially for generic types in generated code. For example, if you previously had an epoxy attribute in java source code with a raw type it may now appear in the generated code with a wildcard type, which may require tweaking the type that is passed to the model.
Additionally, some type checking was improved, for example more accurate validation of proper equals and hashcode implementations.
To use Epoxy with KSP, simply apply it with the ksp gradle plugin instead of kapt (https://github.com/google/ksp/blob/main/docs/quickstart.md). See the new epoxy-kspsample module for an example.
Note that unfortunately the databinding processor does NOT support KSP, simply because Android databinding itself uses KAPT and KSP cannot currently depend on KAPT sources. The code changes are in place to enable KSP with databinding once the databinding plugin from Android supports KSP (although this is unlikely) - alternatively it may be possible to configure the KSP plugin to run after KAPT and depend on its outputs (you're on your own if you want to try that).
Also, parallel processing support was removed because it is not compatible with KSP.
We have also added easy interop with Jetpack Compose via functions in the `epoxy-composeinterop` artifact.
See the epoxy-composesample module for example usage.
# 4.6.4 (September 23, 2021)
- Clean up dependency for the experimental epoxy module
# 4.6.3 (September 11, 2021)
- Add EpoxyModel#preBind hook(#1225)
- Add unbind extension to ItemViewBindingEpoxyHolder (#1223)
- Add missing loadStateFlow to PagingDataEpoxyController (#1209)
# 4.6.2 (June 11, 2021)
Fix Drag n Drop not working in 4.6.1 (#1195)
# 4.6.1 (May 13, 2021)
Adds "epoxyDisableDslMarker" annotation processor flag which you can use to delay migration to the model building scope DLSMarker introduced in 4.6.0 if it is a large breaking change for your project.
Note that this only applies to your project modules that you apply it to, and does not apply to the handful of models that ship with the Epoxy library (like the Carousel or `group` builder).
For example:
```groovy
project.android.buildTypes.all { buildType ->
buildType.javaCompileOptions.annotationProcessorOptions.arguments =
[
epoxyDisableDslMarker : "true",
]
}
```
# 4.6.0 (May 12, 2021)
- View Binder Support (#1175) Bind epoxy models to views outside of a RecyclerView.
### Potentially Breaking
- Use kotlin dsl marker for model building receivers (#1180)
This change uses Kotlin's DSL marker annotation to enforce proper usage of model building extension
functions. You may now need to change some references in your model building code to explicitly reference properties with `this`.
# 4.5.0 (April 13, 2021)
- Fix generated code consistency in builder interfaces (#1166)
- Provided support to invalidate `modelCache` in `PagingDataEpoxyController` (#1161)
- Explicitly add public modifier (#1162)
- Unwrap context to find parent activity in order to share viewpool when using Hilt (#1157)
# 4.4.4 (Mar 24, 2021)
- Provide support for snapshot() function in PagingDataEpoxyController (#1144)
# 4.4.3 (Mar 17, 2021)
- Fixed interface model related regression introduced in the previous release.
# 4.4.2 (Mar 1, 2021)
- Updated package name of the model class generated for an interface
# 4.4.1 (Feb 22, 2021)
- Support for Paging3 (#1126) (Thanks to @osipxd and @anhanh11001!)
- Update KotlinPoet to 1.7.2 (#1117)
# 4.4.0 (Feb 18, 2021)
Bad release, don't use
# 4.3.1 (Dec 2, 2020)
- Fix ANR and view pool resolution in nested group (#1101)
# 4.3.0 (Dec 1, 2020)
- ModelGroupHolder get recycle pool from parent (#1097)
- Add support for `EpoxyModelGroup` in the `EpoxyVisibilityTracker` (#1091)
- Convert EpoxyVisibilityTracker code to Kotlin (#1090)
## Breaking Changes
Note that due to the conversion of EpoxyVisibilityTracker to kotlin you now need to access `EpoxyVisibilityTracker.partialImpressionThresholdPercentage` as a property
`epoxyVisibilityTracker.setPartialImpressionThresholdPercentage(value)` -> `epoxyVisibilityTracker.partialImpressionThresholdPercentage = value`
Also, the ModelGroupHolder improvement required the `ModelGroupHolder#createNewHolder` function to change its signature to accept a `ViewParent` parameter.
If you override `createNewHolder()` anywhere you will need to change it to `createNewHolder(@NonNull ViewParent parent)`
# 4.2.0 (Nov 11, 2020)
- Add notify model changed method (#1063)
- Update to Kotlin 1.4.20-RC and remove dependency on kotlin-android-extensions
# 4.1.0 (Sept 17, 2020)
- Fix some synchronization issues with the parallel Epoxy processing option
- Add view visibility checks to EpoxyVisibilityItem and decouple RecyclerView #1052
# 4.0.0 (Sept 5, 2020)
## New
- Incremental annotation processing for faster builds
- Support for Android Jetpack Paging v3 library in new `epoxy-paging3` artifact
- Model group building with Kotlin DSL (#1012)
- A new annotation processor argument `logEpoxyTimings` can be set to get a detailed breakdown of how long the processors took and where they spent their time (off by default)
- Another new argument `enableParallelEpoxyProcessing` can be set to true to have the annotation processor process annotations and generate files in parallel (via coroutines).
You can enable these processor options in your build.gradle file like so:
```
project.android.buildTypes.all { buildType ->
buildType.javaCompileOptions.annotationProcessorOptions.arguments =
[
logEpoxyTimings : "true",
enableParallelEpoxyProcessing : "true"
]
}
```
Parallel processing can greatly speed up processing time (moreso than the incremental support), but given the hairy nature of parallel processing it is still incubating.
Please report any issues or crashes that you notice.
(We are currently using parallel mode in our large project at Airbnb with no problems.)
- Add options to skip generation of functions for getters, reset, and method overloads to reduce generated code
- New annotation processor options are:
- epoxyDisableGenerateOverloads
- epoxyDisableGenerateGetters
- epoxyDisableGenerateReset
## Fixes
- Synchronize ListUpdateCallback and PagedListModelCache functions (#987)
- Avoid generating bitset checks in models when not needed (reduces code size)
- Fix minor memory leak
## Breaking
- Annotations that previously targeted package elements now target types (classes or interfaces).
This includes: `EpoxyDataBindingPattern`, `EpoxyDataBindingLayouts`, `PackageModelViewConfig`, `PackageEpoxyConfig`
This was necessary to work around an incremental annotation processor issue where annotation on package-info elements are not properly recompiled
- In order to enable incremental annotation processing a change had to be made in how the processor of
`@AutoModel` annotations work. If you use `@AutoModel` in an EpoxyController the annotated Model types
must be either declared in a different module from the EpoxyController, or in the same module in the same java package.
Also make sure you have kapt error types enabled.
However, generally `@AutoModel` is considered legacy and is not recommended. It is a relic of Java Epoxy usage
and instead the current best practice is to use Kotlin with the Kotlin model extension functions to build models.
- Removed support for generating Epoxy models from Litho components
# 4.0.0-beta6 (July 15, 2020)
- PackageModelViewConfig can now be applied to classes and interfaces in addition to package-info.java
# 4.0.0-beta5 (July 9, 2020)
Fixes:
- An occasional processor crash when the option to log timings is enabled
- Incremental annotation processing of databinding models would fail to generate models (#1014)
Breaking!
- The annotation that support databinding, `EpoxyDataBindingLayouts` and `EpoxyDataBindingPattern`,
must now be placed on a class or interface instead of in a `package-info.java` file. The interface
or class must be in Java, Kotlin is not supported. This is necessary to support incremental processing.
Example usage:
```java
package com.example.app;
import com.airbnb.epoxy.EpoxyDataBindingLayouts;
import com.airbnb.epoxy.EpoxyDataBindingPattern;
@EpoxyDataBindingPattern(rClass = R.class, layoutPrefix = "my_view_prefix")
@EpoxyDataBindingLayouts({R.layout.my_model_layout})
interface EpoxyDataBindingConfig {}
```
# 4.0.0-beta4 (June 1, 2020)
Fixes:
- Synchronize ListUpdateCallback and PagedListModelCache functions (#987)
- 4.0.0.beta1 generating duplicate method layout(int) #988
# 4.0.0-beta3 (May 27, 2020)
- Sort functions in generated kotlin extension function files deterministically to prevent generated sources from changing
- Avoid generating bitset checks in models when not needed
- Add options to skip generation of functions for getters, reset, and method overloads to reduce generated code
New annotation processor options are:
- epoxyDisableGenerateOverloads
- epoxyDisableGenerateGetters
- epoxyDisableGenerateReset
These can also be controlled (and overridden) on a per package level with the `PackageModelViewConfig` package annotation.
# 4.0.0-beta1 (May 22, 2020)
- Support for incremental annotation processing as an Aggregating processor (#972)
- Removed Litho support
- A new annotation processor argument `logEpoxyTimings` can be set to get a detailed breakdown of how long the processors took and where they spent their time (off by default)
- Another new argument `enableParallelEpoxyProcessing` can be set to true to have the annotation processor process annotations and generate files in parallel (via coroutines).
You can enable these processor options in your build.gradle file like so:
```
project.android.buildTypes.all { buildType ->
buildType.javaCompileOptions.annotationProcessorOptions.arguments =
[
logEpoxyTimings : "true",
enableParallelEpoxyProcessing : "true"
]
}
```
Parallel processing can greatly speed up processing time (moreso than the incremental support), but given the nature of parallel processing it is still incubating.
Please report any issues or crashes that you notice.
(We are currently using parallel mode in our large project at Airbnb with no problems.)
## Breaking
In order to enable incremental annotation processing a change had to be made in how the processor of
`@AutoModel` annotations work. If you use `@AutoModel` in an EpoxyController the annotated Model types
must be either declared in a different module from the EpoxyController, or in the same module in the same java package.
Also make sure you have kapt error types enabled.
However, generally `@AutoModel` is considered legacy and is not recommended. It is a relic of Java Epoxy usage
and instead the current best practice is to use Kotlin with the Kotlin model extension functions to build models.
# 3.11.0 (May 20, 2020)
- Introduce partial impression visibility states (#973)
- Fix sticky header crash (#976)
# 3.10.0 (May 15, 2020)
- Carousel building with Kotlin DSL (#967)
- Android ViewBinding: added an example in the sample project. (#939)
- Fix setter with default value lookup in kotlin 1.4 (#966)
- Change "result" property name in generated model (#965)
- Add support for Sticky Headers (#842)
- Use measured width/height if it exists in Carousel. (#915)
- Add a getter to EpoxyViewHolder.getHolder(). (#952) (#953)
- Fix visibility tracking during RecyclerView animations (#962)
- Fix leak in ActivityRecyclerPool ((#906)
- Rename ResultCallack to ResultCallback in AsyncEpoxyDiffer (#899)
- Fix incorrect license attributes in POM file (#898)
# 3.9.0 (Dec 17, 2019)
- Fix reading EpoxyDataBindingPattern enableDoNotHash (#837)
- Make EpoxyRecyclerView.setItemSpacingPx() open (#829)
- Use same version for Mockito Core and Inline (#860)
- Minor documentation and variable name updates. (#870)
- Move epoxy-modelfactory tests to their own module (#834)
- Remove executable bit from non-executable files (#864)
- Various repo clean ups and version bumps
# 3.8.0 (Sept 16, 2019)
- Add support for Kotlin delegation via annotated interface properties #812
- Fix checked change crash and improve debug errors #806
- Remove extra space in Kotlin extensions #777
- Update project to AGP 3.5, Kotlin 1.3.50, Gradle 5.6
# 3.7.0 (July 1, 2019)
- **New** Add a method to request visibility check externally (https://github.com/airbnb/epoxy/pull/775)
# 3.6.0 (June 18, 2019)
- **New** Preloader system with glide extensions https://github.com/airbnb/epoxy/pull/766
- **Fixed** model click listener crashing on nested model https://github.com/airbnb/epoxy/pull/767
# 3.5.1 (May 21, 2019)
- Bumped Kotlin to 1.3.31
# 3.5.0 (May 8, 2019)
- **New** Converted EpoxyRecyclerView to Kotlin (you may need to update your usage for this). Also added built in support for `EpoxyRecyclerView#withModels` for easy inline model building with Kotlin.
- **Fixed** Crashes in visibility tracking
# 3.4.2 (April 18, 2019)
- **Fixed** Kotlin default param handling had issues with overloaded functions
# 3.4.1 (April 16, 2019)
- **New** Support kotlin default parameters in @ModelView classes (https://github.com/airbnb/epoxy/pull/722)
# 3.4.0 (April 10, 2019)
- **New** Generate OnModelCheckedChangeListener override for props of type `CompoundButton.OnCheckedChangeListener` (https://github.com/airbnb/epoxy/pull/725)
- **New** Extract ID generation methods to new public IdUtils class (https://github.com/airbnb/epoxy/pull/724)
- **Changed** Reset controller state on failed model build (https://github.com/airbnb/epoxy/pull/720)
- **Changed** Disabled the auto-detach behavior on Carousels by default (https://github.com/airbnb/epoxy/pull/688)
# 3.3.0 (Feb 5, 2019)
- **Fixed** Two issues related to the recent EpoxyModelGroup changes (https://github.com/airbnb/epoxy/pull/676)
# 3.2.0 (Jan 21, 2019)
- **New** Enable recycling of views within EpoxyModelGroup (https://github.com/airbnb/epoxy/pull/657)
- **New** Add support to tracking visibility in nested RecyclerViews (https://github.com/airbnb/epoxy/pull/633)
- **New** Add method to clear cache in paging controller (https://github.com/airbnb/epoxy/pull/586)
- **Fix** Crashes from synchronization in PagedListEpoxyController (https://github.com/airbnb/epoxy/pull/656)
- **Fix** Get onSwipeProgressChanged callbacks on return to original item position (https://github.com/airbnb/epoxy/pull/654)
# 3.1.0 (Dec 4, 2018)
- **Fix** Memory leak in debug mode is removed (https://github.com/airbnb/epoxy/pull/613)
- **Fix** For visibility callbacks, wrong visibility when the view becomes not visible (https://github.com/airbnb/epoxy/pull/619)
# 3.0.0 (Nov 13, 2018)
- **Breaking** Migrated to androidx packages (Big thanks to jeffreydelooff!)
- **Breaking** The `Carousel.Padding` class changed the ordering of its parameters to match Android's ordering of "left, top, right, bottom". (https://github.com/airbnb/epoxy/pull/536 thanks to martinbonnin)
This change won't break compilation, so you _must_ manually change your parameter ordering, otherwise you will get unexpected padding results.
# 2.19.0 (Oct 18, 2018)
This release adds built in support for monitoring visibility of views in the RecyclerView. (https://github.com/airbnb/epoxy/pull/560)
Usage instructions and details are in the wiki - https://github.com/airbnb/epoxy/wiki/Visibility-Events
Huge thanks to Emmanuel Boudrant for contributing this!
# 2.18.0 (Sep 26, 2018)
- **New** A new `PagedListEpoxyController` to improve integration with the Android Paging architecture component (#533 Thanks to Yigit!)
With this change the old `PagingEpoxyController` has been deprecated, and [the wiki](https://github.com/airbnb/epoxy/wiki/Paging-Support) is updated.
- **New** Add databinding option to not auto apply DoNotHash (#539)
- **Fixed** Fix AsyncEpoxyController constructor to correctly use boolean setting (#537)
- **Fixed** `app_name` is removed from module manifests (#543 Thanks @kettsun0123!)
# 2.17.0 (Sep 6, 2018)
- **New** Add support for setting the Padding via resource or directly in dp (https://github.com/airbnb/epoxy/pull/528 Thanks to pwillmann!)
- **Fixed** Strip kotlin metadata annotation from generated classes (https://github.com/airbnb/epoxy/pull/523)
- **Fixed** Reflect the annotations declared in constructor params (https://github.com/airbnb/epoxy/pull/519 Thanks to Shaishav Gandhi!)
# 2.16.4 (Aug 29, 2018)
- **New** `EpoxyAsyncUtil` and `AsyncEpoxyController` make it easier to use Epoxy's async behavior out of the box
- **New** Epoxy's background diffing posts messages back to the main thread asynchronously so they are not blocked by waiting for vsync
# 2.16.3 (Aug 24, 2018)
- **New** Add `AsyncEpoxyController` for easy access to async support. Change background diffing to post asynchronously to the main thread (https://github.com/airbnb/epoxy/pull/509)
# 2.16.2 (Aug 23, 2018)
- **Fix** Kotlin lambdas can be used in model constructors (https://github.com/airbnb/epoxy/pull/501)
- **New** Added function to check whether a model build is pending (https://github.com/airbnb/epoxy/pull/506)
# 2.16.1 (Aug 15, 2018)
- **Fix** Update EpoxyController async model building so threading works with tests (https://github.com/airbnb/epoxy/pull/504)
# 2.16.0 (Aug 7, 2018)
- **New** EpoxyController now supports asynchronous model building and diffing by allowing you to provide a custom Handler to run these tasks. See the [wiki](https://github.com/airbnb/epoxy/wiki/Epoxy-Controller#asynchronous-support) for more details.
- **New** The `EpoxyController#addModelBuildListener` method was added to support listening for when model changes are dispatched to the recyclerview.
# 2.15.0 (July 29, 2018)
- **New** Added kotlin sample code for building models. Updated wiki with info (https://github.com/airbnb/epoxy/wiki/Kotlin-Model-Examples)
- **Fix** Generated kotlin extension functions now work with Models with type variables (https://github.com/airbnb/epoxy/pull/478)
- **Fix** Backup is not enabled in manifest now (https://github.com/airbnb/epoxy/pull/481)
- **Fix** Click listener setter on generated model has correct nullability annotation (https://github.com/airbnb/epoxy/pull/458)
- **Fix** Avoid kotlin crash using toString on lambdas (https://github.com/airbnb/epoxy/pull/482)
- **Fix** If EpoxyModelGroup has annotations the generated class now calls super methods correctly. (https://github.com/airbnb/epoxy/pull/483)
# 2.14.0 (June 27, 2018)
- **New** Experimental support for creating Epoxy models from arbitrary data formats (#450)
# 2.13.0 (June 19, 2018)
- **Fix** Reduce memory usage in model groups and differ (#433)
- **Fix** Support for wildcards in private epoxy attributes (#451)
- **Fix** Generated Kotlin Extensions Don't Adhere to Constructor Nullability (#449)
- **Fix** Infinite loop in annotation processor (#447)
# 2.12.0 (April 18, 2018)
- **Breaking** Several updates to the Paging Library integration were made (https://github.com/airbnb/epoxy/pull/421)
- The `PagingEpoxyController` class had the methods `setNumPagesToLoad` and `setPageSizeHint` removed
- Page hints are now taken from the `Config` object off of the PagedList. See the `setConfig` javadoc for information on how config values are used: https://github.com/airbnb/epoxy/blob/master/epoxy-paging/src/main/java/com/airbnb/epoxy/paging/PagingEpoxyController.java#L220
- Several tweaks were made to how the page size and prefetch distance affect model rebuilding. Take some time to make sure your config values make sense and produce good results for your use case
- A crash on empty list was fixed (https://github.com/airbnb/epoxy/issues/420)
- **New** The [Paris](https://github.com/airbnb/paris) library is now officially supported to allow dynamically styling RecyclerView items though Epoxy models. See [the wiki](https://github.com/airbnb/epoxy/wiki/Paris-Integration-(Dynamic-Styling)) for more info.
# 2.11.0 (April 7, 2018)
- **Fix** Make databinding work with Android Studio 3.1 (https://github.com/airbnb/epoxy/pull/418)
- Make `EpoxyController#isBuildingModels` public (https://github.com/airbnb/epoxy/pull/406
# 2.10.0 (February 25, 2018)
- **Improved** Allow the `Model_` class suffix for models generated via `@ModelView` to be customized (https://github.com/airbnb/epoxy/pull/402 Big thanks to geralt-encore!)
# 2.9.0 (January 29, 2018)
- **Improved** Global defaults for EpoxyController settings. Set duplicate filtering and exception handlers for all your controllers. (https://github.com/airbnb/epoxy/pull/394)
- **Improved** Add `@NonNull` annotations in EpoxyModel for better Kotlin interop
- **Fixed** Model click listeners now rebind correctly on partial model diffs (https://github.com/airbnb/epoxy/pull/393)
- **Fixed** Update Android Paging library to fix placeholder support (Thanks @wkranich! https://github.com/airbnb/epoxy/pull/360)
- **Fixed** Improve error message for inaccessible private fields (https://github.com/airbnb/epoxy/pull/388)
# 2.8.0 (December 22, 2017)
- **New** Use `@ModelProp` directly on fields to avoid creating a setter (https://github.com/airbnb/epoxy/pull/343)
- **New** Set EpoxyRecyclerView item spacing via xml attribute (https://github.com/airbnb/epoxy/pull/364)
- **New** More flexibility over setting Carousel padding values (https://github.com/airbnb/epoxy/pull/369)
- **New** Allow custom EpoxyModelGroup root view (https://github.com/airbnb/epoxy/pull/370)
- **Fixed** Public visibility settings of the Carousel snap helper settings (https://github.com/airbnb/epoxy/pull/356)
- **Fixed** Add more nullability annotations to better support Kotlin
- **Fixed** Saving view state now works better (https://github.com/airbnb/epoxy/pull/367)
# 2.7.3 (November 21, 2017)
- **Fixed** When a model changed and a partial update was bound to an existing view the wrong values could be set for prop groups (https://github.com/airbnb/epoxy/pull/347)
# 2.7.2 (October 28, 2017)
- **Fixed** Using `EpoxyDataBindingPattern` could result in the wrong package being used for the BR class in generated models.
# 2.7.1 (October 24, 2017)
Several fixes:
- https://github.com/airbnb/epoxy/pull/332
- https://github.com/airbnb/epoxy/pull/329
- https://github.com/airbnb/epoxy/pull/330
- https://github.com/airbnb/epoxy/pull/331
# 2.7.0 (October 17, 2017)
* **New** If a `@ModelView` generated model has a custom base class the generated model will now inherit constructors from the base class (https://github.com/airbnb/epoxy/pull/315)
* **New** Use the `EpoxyDataBindingPattern` annotation to specify a naming pattern for databinding layouts. This removes the need to declare every databinding layout explicitly ([Wiki](https://github.com/airbnb/epoxy/wiki/Data-Binding-Support#automatic-based-on-naming-pattern) - https://github.com/airbnb/epoxy/pull/319)
* **New** If a view with `@ModelView` implements an interface then the generated model will implement a similar interface, enabling polymorphism with models. [Wiki](https://github.com/airbnb/epoxy/wiki/Generating-Models-from-View-Annotations#view-interfaces)
* **Improvement** `PagingEpoxyController` now has getters to access the underlying data lists (Thanks to @pcqpcq - https://github.com/airbnb/epoxy/pull/317)
* **Improvement** `EpoxyModelGroup` now supports partial rebinds (https://github.com/airbnb/epoxy/pull/316)
# 2.6.0 (October 10, 2017)
* **Improvement** If a `OnModelClickListener` is used it will not be called if a view is clicked while it is being removed or otherwise has no position (https://github.com/airbnb/epoxy/issues/293 - Thanks @niccorder!)
* **New** `EpoxyRecyclerView` and `Carousel` provide out of the box integration with Epoxy along with other enhancements over regular RecyclerView (https://github.com/airbnb/epoxy/wiki/EpoxyRecyclerView)
* **New** `EpoxyPagingController` provides integration with the Android Paging architecture component as well as normal, large lists of items (https://github.com/airbnb/epoxy/wiki/Large-Data-Sets)
#### Kotlin
* **Improvement** Disable kotlin extension function generation with the annotation processor flag `disableEpoxyKotlinExtensionGeneration` (https://github.com/airbnb/epoxy/pull/309)
* **Fix** If a model has a non empty constructor the generated extension function will now use it.
# 2.5.1 (October 2, 2017)
* **Fixed** The wrong import was being generated for models using a view holder in 2.5.0 (https://github.com/airbnb/epoxy/pull/294)
* **Fixed** Fix generated code failing to compile if a subclass of View.OnClickListener is used as an attribute (https://github.com/airbnb/epoxy/pull/296)
# 2.5.0 (September 14, 2017)
* **New Feature** Epoxy now generates a Kotlin DSL to use when building models in your EpoxyController! See [the wiki](https://github.com/airbnb/epoxy/wiki/Epoxy-Controller#usage-with-kotlin) for details
* **New Feature** You can use the `autoLayout` parameter in `@ModelView` instead of needing to create a layout resource for `defaultLayout`. Epoxy will then create your view programmatically (https://github.com/airbnb/epoxy/pull/282).
**Breaking**
* The `onSwipeProgressChanged` callback in `EpoxyTouchHelper` had a `Canvas` parameter added (https://github.com/airbnb/epoxy/pull/280). You will need to update any of your usages to add this. Sorry for the inconvenience; this will hopefully help you add better swipe animations.
# 2.4.0 (September 4, 2017)
* **Improvement** If you are setting options on a @ModelProp and have no other annotation parameters you can now omit the explicit `options = ` param name (https://github.com/airbnb/epoxy/pull/268)
* **Improvement** If you are using `@TextProp` you can now specify a default string via a string resource (https://github.com/airbnb/epoxy/pull/269)
* **Fixed** EpoxyModelGroup was not binding model click listeners correctly (https://github.com/airbnb/epoxy/pull/267)
* **Fixed** A model created with @ModelView could fail to compile if it had nullable prop overloads (https://github.com/airbnb/epoxy/pull/274)
#### Potentially Breaking Fix
A model created with @ModelView with a click listener had the wrong setter name for the model click listener overload (https://github.com/airbnb/epoxy/pull/275)
If you were setting this you will need to update the setter name. If you were setting the click listener to null you may now have to cast it.
# 2.3.0 (August 16, 2017)
* **New** An `AfterPropsSet` annotation for use in `@ModelView` classes. This allows initialization work to be done after all properties are bound from the model. (https://github.com/airbnb/epoxy/pull/242)
* **New** Annotations `TextProp` and `CallbackProp` as convenient replacements for `ModelProp`. (https://github.com/airbnb/epoxy/pull/260)
* **New** Easy support for dragging and swiping via the `EpoxyTouchHelper` class. https://github.com/airbnb/epoxy/wiki/Touch-Support
* **Change** Added the method `getRootView` to the view holder class in `EpoxyModelGroup` and made the bind methods on `EpoxyModelGroup` non final. This allows access to the root view of the group.
* **Change** Generated models will now inherit class annotations from the base class (https://github.com/airbnb/epoxy/pull/255 Thanks geralt-encore!)
# 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).
* **New**: Support for Litho. Epoxy will now generate an EpoxyModel for Litho Layout Specs. See more details in [the wiki](https://github.com/airbnb/epoxy/wiki/Litho-Support).
* **New**: Support for implicitly adding AutoModels to an EpoxyController, this let's you drop the extra `.addTo(this)` line. More details and instructions [here](https://github.com/airbnb/epoxy/wiki/Epoxy-Controller#implicit-adding)
# 2.0.0 (March 25, 2017)
* **New**: The `EpoxyController` class helps you manage even models better. This should be used instead of the original `EpoxyAdapter` in most places. Read more about `EpoxyController` in [the wiki](https://github.com/airbnb/epoxy/wiki/Epoxy-Controller).
* **Change**: In the new EpoxyController, the diffing algorithm uses both `equals` and `hashCode` on each model to check for changes. This is a change from the EpoxyAdapter where only `hashCode` was used. Generated models have both hashCode and equals implemented properly already, but if you have any custom hashCode implementations in your models make sure you have equals implemented as well.
* **New**: Models that have a `View.OnClickListener` as an EpoxyAttribute will now have an overloaded setter on the generated model that allows you to set a click listener that will return the model, view, and adapter position. **Upgrade Note** If you were setting a click listener value to null anywhere you will need to now cast that to `View.OnClickListener` because of the new overloaded method.
* **New**: Attach an onBind/onUnbind listener directly to a model instead of overriding the onModelBound method. Generated models will have methods created to set this listener and handle the callback for you.
* **New**: Support for creating models in Kotlin (Thanks to @geralt-encore! https://github.com/airbnb/epoxy/pull/144)
* **New**: `EpoxyModelWithView` supports creating a View programmatically instead of inflating from XML.
* **New**: `EpoxyModelGroup` supports grouping models together in arbitrary formations.
* **New**: Instead of setting attribute options like `@EpoxyAttribute(hash = false)` you should now do `@EpoxyAttribute(DoNotHash)`. You can also set other options like that.
* **New**: Annotation processor options can now be set via gradle instead of with `PackageEpoxyConfig`
* **New**: In an EpoxyController, if a model with the same id changes state Epoxy will include its previous state as a payload in the change notification. The new model will have its `bind(view, previouslyBoundModel)` method called so it can compare what changed since the previous model, and so it can update the view with only the data that changed.
# 1.7.5 (Feb 21, 2017)
* **New**: Models inherit layouts specified in superclass `@EpoxyModelClass` annotations [#119](https://github.com/airbnb/epoxy/pull/119)
* **New**: Support module configuration options [#124](https://github.com/airbnb/epoxy/pull/124)
# 1.6.2 (Feb 8, 2017)
* New: Support layout resource annotations in library projects (https://github.com/airbnb/epoxy/pull/116)
# 1.6.1 (Feb 6, 2017)
* Allow the default layout resource to be specified in the EpoxyModelClass class annotation [(#109)](https://github.com/airbnb/epoxy/pull/109) [(#111)](https://github.com/airbnb/epoxy/pull/111)
* Allow the `createNewHolder` method to be omitted and generated automatically [(#105)](https://github.com/airbnb/epoxy/pull/105)
* Generate a subclass for abstract model classes if the EpoxyModelClass annotation is present [(#105)](https://github.com/airbnb/epoxy/pull/105)
* Allow strings as model ids [(#107)](https://github.com/airbnb/epoxy/pull/107)
* Add instructions to readme for avoiding memory leaks [(#106)](https://github.com/airbnb/epoxy/pull/106)
* Add model callbacks for view attached/detached from window, and onFailedToRecycleView [(#104)](https://github.com/airbnb/epoxy/pull/104)
* Improve documentation on model unbind behavior [(#103)](https://github.com/airbnb/epoxy/pull/103)
* Fix generated methods from super classes that have var args [(#100)](https://github.com/airbnb/epoxy/pull/100)
* Remove apt dependency [(#95)](https://github.com/airbnb/epoxy/pull/95)
* Add `removeAllModels` method to EpoxyAdapter [(#94)](https://github.com/airbnb/epoxy/pull/94)
* Use actual param names when generating methods from super classes [(#85)](https://github.com/airbnb/epoxy/pull/85)
# 1.5.0 (11/21/2016)
* Fixes models being used in separate modules
* Generates a `reset()` method on each model to reset annotated fields to their defaults.
* Changes `@EpoxyAttribute(hash = false)` to still differentiate between null and non null values in the hashcode implementation
* Adds a `notifyModelChanged` method to EpoxyAdapter that allows a payload to be specified
* Generates a `toString()` method on all generated model classes that includes the values of all annotated fields.
# 1.4.0 (10/13/2016)
* Optimizations to the diffing algorithm
* Setters on generated classes are not created if an @EpoxyAttribute field is marked as `final`
* Adds @EpoxyModelClass annotation to force a model to have a generated class, even if it doesn't have any @EpoxyAttribute fields
* Fix to not generate methods for package private @EpoxyAttribute fields that are in a different package from the generated class
* Have generated classes duplicate any super methods that have the model as the return type to help with chaining
# 1.3.0 (09/15/2016)
* Add support for using the view holder pattern with models. See the readme for more information.
* Throw an exception if `EpoxyAdapter#notifyDataSetChanged()` is called when diffing is enabled. It doesn't make sense to allow this alongside diffing, and calling this is most likely to be an accidental mixup with `notifyModelsChanged()`.
* Some performance improvements with the diffing algorithm.
# 1.2.0 (09/07/2016)
* Change signature of `EpoxyAdapter#onModelBound` to include the model position
* Fix EpoxyModel hashcode to include the layout specified by `getDefaultLayout`
* Enforce that the id of an `EpoxyModel` cannot change once it has been added to the adapter
* Add optional hash parameter to the `EpoxyAttribute` annotation to exclude a field from being included in the generated hashcode method.
# 1.1.0 (08/24/2016)
* Initial release
================================================
FILE: CONTRIBUTING.MD
================================================
# Epoxy is an Open Source Project
Pull requests are welcome! We'd love help improving this library.
We have a code style setting for the project (checkstyle for Java, ktlint for Kotlin). Please run `Reformat Code` in Android Studio (or Intellij) on changed files before pushing them. Alternatively for Kotlin you can use [ktlint](https://ktlint.github.io/) tasks: check Kotlin code formatting with`./gradlew ktlint` and reformat all Kotlin code with `./gradlew ktlintformat`.
Also, run `./gradlew check` locally to make sure that style checks and tests pass.
If you update the model annotation processor you may find the `UpdateProcessorTestResults.kt` script very useful for updating the existing tests with your changes. (run it with kscript)
- Run ./gradlew testDebug first to get test failures, then run `kscript UpdateProcessorTestResources.kt` to updates sources
- You may have to repeat this cycle several times for all tests to be updated.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2018 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.
================================================
FILE: README.md
================================================
[](https://travis-ci.com/github/airbnb/epoxy)
[](https://maven-badges.herokuapp.com/maven-central/com.airbnb.android/epoxy)
[](https://github.com/airbnb/epoxy/blob/master/LICENSE)

# Epoxy
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](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](https://github.com/airbnb/epoxy/wiki/Sample-App)
## Installation
Gradle is the only supported build configuration, so just add the dependency to your project `build.gradle` file:
```groovy
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 : [](https://maven-badges.herokuapp.com/maven-central/com.airbnb.android/epoxy)
See the [releases page](https://github.com/airbnb/epoxy/releases) for up to date release versions and details
#### Kotlin with KAPT
If you are using Kotlin with KAPT you should also add
```groovy
apply plugin: 'kotlin-kapt'
kapt {
correctErrorTypes = true
}
```
so that `AutoModel` annotations work properly. More information [here](https://github.com/airbnb/epoxy/wiki/Epoxy-Controller#usage-with-kotlin)
Also, make sure to use `kapt` instead of `annotationProcessor` in your dependencies in the `build.gradle` file.
#### Kotlin with KSP (Recommended)
KSP (Kotlin Symbol Processing) is recommended over KAPT as it is significantly faster.
Add the KSP plugin to your root `build.gradle`:
```groovy
plugins {
id 'com.google.devtools.ksp' version "$KSP_VERSION" apply false
}
```
Then apply it in your module's `build.gradle`:
```groovy
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:
```groovy
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](https://github.com/airbnb/epoxy/tree/master/epoxy-kspsample) module for a complete working example.
## Library Projects
If you are using layout resources in Epoxy annotations then for library projects add [Butterknife's gradle plugin](https://github.com/JakeWharton/butterknife#library-projects) to your `buildscript`.
```groovy
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:10.1.0'
}
}
```
and then apply it in your module:
```groovy
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
```
Now make sure you use R2 instead of R inside all Epoxy annotations.
```java
@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](https://github.com/airbnb/epoxy/wiki/Generating-Models-from-View-Annotations).
## 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
Epoxy generates models for you based on your view or layout. Generated model classes are suffixed with an underscore (`_`) are used directly in your EpoxyController classes.
#### From Custom Views
Add the `@ModelView` annotation on a view class. Then, add a "prop" annotation on each setter method to mark it as a property for the model.
```java
@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.
[More Details](https://github.com/airbnb/epoxy/wiki/Generating-Models-from-View-Annotations)
#### From DataBinding
If you use Android DataBinding you can simply set up your xml layouts like normal:
```xml
```
Then, create an interface or class in any package and add an `EpoxyDataBindingLayouts` annotation to declare your databinding layouts.
```java
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_`.
[More Details](https://github.com/airbnb/epoxy/wiki/Data-Binding-Support)
#### From ViewHolders
If you use xml layouts without databinding you can create a model class to do the binding.
```java
@EpoxyModelClass(layout = R.layout.header_view)
public abstract class HeaderModel extends EpoxyModelWithHolder {
@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.
[More Details](https://github.com/airbnb/epoxy/wiki/ViewHolder-Models)
### 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 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.
```java
public class PhotoController extends Typed2EpoxyController, Boolean> {
@AutoModel HeaderModel_ headerModel;
@AutoModel LoaderModel_ loaderModel;
@Override
protected void buildModels(List 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);
}
}
```
#### Or with Kotlin
An extension function is generated for each model so we can write this:
```kotlin
class PhotoController : Typed2EpoxyController, Boolean>() {
override fun buildModels(photos: List, 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") }
}
}
```
### Integrating with RecyclerView
Get the backing adapter off the EpoxyController to set up your RecyclerView:
```java
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](https://github.com/airbnb/epoxy/wiki/EpoxyRecyclerView) integration is easier.
```java
epoxyRecyclerView.setControllerAndBuildModels(new MyController());
// Request a model build on the recyclerview when data changes
epoxyRecyclerView.requestModelBuild();
```
#### Kotlin
Or use [Kotlin Extensions](https://github.com/airbnb/epoxy/wiki/EpoxyRecyclerView#kotlin-extensions) to simplify further and remove the need for a controller class.
```kotlin
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") }
}
}
```
### More Reading
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](https://github.com/airbnb/epoxy/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.
## 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.
## License
```
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.
```
================================================
FILE: RELEASING.md
================================================
Releasing
========
1. Bump the VERSION_NAME property in `gradle.properties` based on Major.Minor.Patch naming scheme
2. Update `CHANGELOG.md` for the impending release.
3. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the version you set in step 1)
4. Add your sonatype login information under gradle properties mavenCentralUsername and mavenCentralPassword in your local user gradle.properties file
5. Make sure you have a gpg signing key configured (https://vanniktech.github.io/gradle-maven-publish-plugin/central/#secrets)
5. Run `./gradlew publish` to build the artifacts and publish them to maven
7. Open PR on Github, merge, and publish release through Github UI.
Publishing a release to an internal repository
========
To publish an internal release to an Artifactory repository:
1. Set credential values for ARTIFACTORY_USERNAME and ARTIFACTORY_PASSWORD in your local gradle.properties
2. Set values for ARTIFACTORY_RELEASE_URL (and optionally ARTIFACTORY_SNAPSHOT_URL if you are publishing a snapshot)
3. ./gradlew publishAllPublicationsToAirbnbArtifactoryRepository -PdoNotSignRelease=true --no-configuration-cache
4. "-PdoNotSignRelease=true" is optional, but we don't need to sign artifactory releases and this allows everyone to publish without setting up a gpg key
If you need to publish to a different repository, look at the configuration in 'publishing.gradle'
to see how to configure additional repositories.
Maven Local Installation
=======================
If testing changes locally, you can install to mavenLocal via `./gradlew publishToMavenLocal`
================================================
FILE: UpdateProcessorTestResources.kt
================================================
#!/usr/bin/env kscript
@file:DependsOn("org.jsoup:jsoup:1.13.1")
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.io.File
fun main() {
val testResultHtmlRegex = Regex("/build/reports/tests/.*/classes/.*\\.html")
File(".")
.walk()
.filter { it.isFile }
.filter { it.path.contains(testResultHtmlRegex) }
.forEach { updateTestClass(it) }
}
fun updateTestClass(testReport: File) {
val doc: Document = Jsoup.parse(testReport, "UTF-8")
// Failing processor tests have their output in a block
doc.getElementsByTag("pre")
.filter { element ->
// A failing block contains the text "Source declared the same top-level types of an expected source, but
// didn't match exactly."
element.text().contains("Source declared the same top-level types of an expected source")
}.map { it.text() }
.forEach { failingTestText ->
updateIndividualTest(failingTestText)
}
}
private fun updateIndividualTest(failingTestText: String) {
val expectedFile = expectedFileRegex
.find(failingTestText)
?.groupValues
?.getOrNull(1)
?.let { filePath ->
// The test copies the source file to the build folder. We need to modify the original file to update its expected source
File(
filePath.replace(
"/build/intermediates/sourceFolderJavaResources/debug/",
"/src/test/resources/"
)
)
}
?.takeIf { it.isFile }
?: error("Count not find expected file in $failingTestText")
// The error message includes the source code that was generated.
// Actual Source:
//=================
// [code here]
//
// javaSources was: [com.google.testing.compile.JavaFileObjects$ResourceSourceJavaFileObject[file:/Users/elihart/repos/epoxy/epoxy-modelfactorytest/build/intermediates/sourceFolderJavaResources/debug/GroupPropMultipleSupportedAttributeDifferentNameModelView.java]]
// at com.airbnb.epoxy.ProcessorTestUtils.assertGeneration(ProcessorTestUtils.kt:33)
// ...
val actualSource = failingTestText.substringAfter(
"""
Actual Source:
=================
""".trimIndent()
).substringBefore("javaSources was:")
.substringBefore("object was:")
expectedFile.writeText(actualSource)
println("Updated test source ${expectedFile.path.substringAfter("/epoxy/")}")
}
// We expect to see a line like:
// Expected file:
// Which tells us where the original processor test file lives
val expectedFileRegex = Regex("Expected file: <(.*)>")
================================================
FILE: blessedDeps.gradle
================================================
/**
* "Blessed" dependencies give us the ability to force a dependency(s) version to be consistent
* for packaging a library. In turn, this also allows the ability to retract the forced update
* based off build type/flavor which reduces regressions caused by conflicts.
*
* As an added bonus, we don't bloat our project build file by abstracting dependencies into its own
* gradle file, then applying it back in as necessary.
*/
rootProject.ext.TARGET_SDK_VERSION = 30
rootProject.ext.COMPILE_SDK_VERSION = 33
rootProject.ext.MIN_SDK_VERSION = 14
rootProject.ext.COMPOSE_MIN_SDK_VERSION = 21
rootProject.ext.PARIS_MIN_SDK_VERSION = 21
rootProject.ext.ANDROIDX_ANNOTATION = "1.5.0"
rootProject.ext.ANDROIDX_APPCOMPAT = "1.5.1"
rootProject.ext.ANDROIDX_CARDVIEW = "1.0.0"
rootProject.ext.ANDROIDX_CORE_KTX = "1.3.2"
rootProject.ext.ANDROIDX_DATABINDING_ADAPTERS = "3.2.1"
rootProject.ext.ANDROIDX_DATABINDING_COMPILER = "3.2.1"
rootProject.ext.ANDROIDX_DATABINDING_LIBRARY = "3.2.1"
rootProject.ext.ANDROIDX_ESPRESSO_CORE = "3.5.1"
rootProject.ext.ANDROIDX_FRAGMENT_TESTING = "1.3.3"
rootProject.ext.ANDROIDX_LEGACY = "1.0.0"
rootProject.ext.ANDROIDX_MATERIAL = "1.3.0"
rootProject.ext.ANDROIDX_PAGING = "2.0.0"
rootProject.ext.ANDROIDX_PAGING3 = "3.1.1"
rootProject.ext.ANDROIDX_RECYCLERVIEW = "1.3.0-rc01"
rootProject.ext.ANDROIDX_ROOM = "2.5.0-beta01"
rootProject.ext.ANDROIDX_RUNTIME = "2.3.1"
rootProject.ext.ANDROIDX_VERSIONED_PARCELABLE = "1.1.1"
rootProject.ext.ANDROID_ARCH_TESTING = "2.1.0"
rootProject.ext.ANDROID_DATA_BINDING = "1.3.1"
rootProject.ext.ANDROID_RUNTIME_VERSION = "4.1.1.4"
rootProject.ext.ANDROID_TEST_RUNNER = "1.5.2"
rootProject.ext.ANDROID_TEST_RULES = "1.5.0"
rootProject.ext.ASSERTJ_VERSION = "1.7.1"
rootProject.ext.AUTO_VALUE_VERSION = "1.7.4"
rootProject.ext.GLIDE_VERSION = "4.12.0"
rootProject.ext.GOOGLE_TESTING_COMPILE_VERSION = "0.23.0"
rootProject.ext.INCAP_VERSION = "0.3"
rootProject.ext.JUNIT_VERSION = "4.13.2"
rootProject.ext.KOTLIN_COROUTINES_VERSION = "1.6.4"
rootProject.ext.LOTTIE_VERSION = "2.8.0"
rootProject.ext.MOCKITO_VERSION = "5.20.0"
rootProject.ext.PARIS_VERSION = "2.2.1"
rootProject.ext.ROBOLECTRIC_VERSION = "4.9.2"
rootProject.ext.SQUARE_JAVAPOET_VERSION = "1.13.0"
rootProject.ext.SQUARE_KOTLINPOET_VERSION = "1.12.0"
rootProject.ext.COMPOSE_VERSION = "1.4.2"
rootProject.ext.COMPOSE_ACTIVITY_VERSION = "1.6.0"
rootProject.ext.KOTLINX_LIFECYCLE_RUNTIME_VERSION = "2.5.1"
rootProject.ext.XPROCESSING_VERSION = "2.8.4"
rootProject.ext.KOTLIN_TESTING_COMPILE_VERSION = '0.11.0'
rootProject.ext.LIFECYCLE_VIEWMODEL = '2.6.1'
rootProject.ext.deps = [
activityCompose : "androidx.activity:activity-compose:$COMPOSE_ACTIVITY_VERSION",
androidAnnotations : "androidx.annotation:annotation:$ANDROIDX_ANNOTATION",
androidAppcompat : "androidx.appcompat:appcompat:$ANDROIDX_APPCOMPAT",
androidArchCoreTesting : "androidx.arch.core:core-testing:$ANDROID_ARCH_TESTING",
androidCardView : "androidx.cardview:cardview:$ANDROIDX_CARDVIEW",
androidCoreKtx : "androidx.core:core-ktx:$ANDROIDX_CORE_KTX",
androidDesignLibrary : "com.google.android.material:material:$ANDROIDX_MATERIAL",
androidEspressoCore : "androidx.test.espresso:espresso-core:$ANDROIDX_ESPRESSO_CORE",
androidFragmentTesting : "androidx.fragment:fragment-testing:$ANDROIDX_FRAGMENT_TESTING",
androidLifecycleRuntimeKtx : "androidx.lifecycle:lifecycle-runtime-ktx:$KOTLINX_LIFECYCLE_RUNTIME_VERSION",
androidPaging3Component : "androidx.paging:paging-runtime:$ANDROIDX_PAGING3",
androidPagingComponent : "androidx.paging:paging-runtime:$ANDROIDX_PAGING",
androidRecyclerView : "androidx.recyclerview:recyclerview:$ANDROIDX_RECYCLERVIEW",
androidRuntime : "com.google.android:android:$ANDROID_RUNTIME_VERSION",
androidTestCore : "androidx.test:core:1.3.0",
androidTestExtJunitKtx : "androidx.test.ext:junit-ktx:1.1.2",
androidTestRules : "androidx.test:rules:$ANDROID_TEST_RULES",
androidTestRunner : "androidx.test:runner:$ANDROID_TEST_RUNNER",
assertj : "org.assertj:assertj-core:$ASSERTJ_VERSION",
autoValue : "com.google.auto.value:auto-value:$AUTO_VALUE_VERSION",
composeMaterial : "androidx.compose.material:material:$COMPOSE_VERSION",
composeUi : "androidx.compose.ui:ui:$COMPOSE_VERSION",
composeUiTooling : "androidx.compose.ui:ui-tooling:$COMPOSE_VERSION",
dataBindingAdapters : "androidx.databinding:databinding-adapters:$ANDROIDX_DATABINDING_ADAPTERS",
dataBindingLibrary : "androidx.databinding:databinding-library:$ANDROIDX_DATABINDING_LIBRARY",
glide : "com.github.bumptech.glide:glide:$GLIDE_VERSION",
googleTestingCompile : "com.google.testing.compile:compile-testing:$GOOGLE_TESTING_COMPILE_VERSION",
incapProcessor : "net.ltgt.gradle.incap:incap-processor:$INCAP_VERSION",
incapRuntime : "net.ltgt.gradle.incap:incap:$INCAP_VERSION",
junit : "junit:junit:$JUNIT_VERSION",
kotlinCoroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$KOTLIN_COROUTINES_VERSION",
kotlinCoroutinesTest : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$KOTLIN_COROUTINES_VERSION",
kotlinxMetadata : "org.jetbrains.kotlin:kotlin-metadata-jvm:$KOTLIN_VERSION",
lottie : "com.airbnb.android:lottie:$LOTTIE_VERSION",
mockito : "org.mockito:mockito-core:$MOCKITO_VERSION",
paris : "com.airbnb.android:paris:$PARIS_VERSION",
parisProcessor : "com.airbnb.android:paris-processor:$PARIS_VERSION",
robolectric : "org.robolectric:robolectric:$ROBOLECTRIC_VERSION",
squareJavaPoet : "com.squareup:javapoet:$SQUARE_JAVAPOET_VERSION",
squareKotlinPoet : "com.squareup:kotlinpoet:$SQUARE_KOTLINPOET_VERSION",
kotlinPoetJavaInterop : "com.squareup:kotlinpoet-javapoet:$SQUARE_KOTLINPOET_VERSION",
kotlinPoetKspInterop : "com.squareup:kotlinpoet-ksp:$SQUARE_KOTLINPOET_VERSION",
versionedParcelable : "androidx.versionedparcelable:versionedparcelable:$ANDROIDX_VERSIONED_PARCELABLE",
ksp : "com.google.devtools.ksp:symbol-processing-api:$KSP_VERSION",
kspAaEmbeddable : "com.google.devtools.ksp:symbol-processing-aa-embeddable:$KSP_VERSION",
kspImpl : "com.google.devtools.ksp:symbol-processing:$KSP_VERSION",
xProcessing : "androidx.room:room-compiler-processing:$XPROCESSING_VERSION",
xProcessingTesting : "androidx.room:room-compiler-processing-testing:$XPROCESSING_VERSION",
kotlinCompileTesting : "dev.zacsweers.kctfork:ksp:$KOTLIN_TESTING_COMPILE_VERSION",
kotlinAnnotationProcessingEmbeddable: "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable:$KOTLIN_VERSION",
lifecycleViewmodel : "androidx.lifecycle:lifecycle-viewmodel:$LIFECYCLE_VIEWMODEL",
lifecycleViewmodelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VIEWMODEL",
]
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.KOTLIN_VERSION = "2.2.21"
ext.ANDROID_PLUGIN_VERSION = '8.13.0'
ext.KSP_VERSION = '2.3.3'
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath "com.android.tools.build:gradle:$ANDROID_PLUGIN_VERSION"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$KOTLIN_VERSION"
// Upload with: (see RELEASING.md)
// ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.35.0'
// Dokka is needed on classpath for vanniktech publish plugin
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.6.10"
}
}
plugins {
// Run ./gradlew dependencyUpdates to see available version updates
id 'com.github.ben-manes.versions' version '0.42.0'
id "com.google.devtools.ksp" version "$KSP_VERSION"
}
allprojects {
repositories {
google()
mavenCentral()
}
// Prevent javadoc task complaining about errors with kotlin files
tasks.withType(Javadoc) {
excludes = ['**/*.kt']
}
}
subprojects { project ->
apply from: "$rootDir/blessedDeps.gradle"
apply plugin: 'com.github.ben-manes.versions'
apply from: "${project.rootDir}/ktlint.gradle"
afterEvaluate {
if (project.tasks.findByName('check')) {
check.dependsOn('ktlint')
}
if (project.extensions.findByType(com.android.build.gradle.LibraryExtension.class) != null) {
project.android.libraryVariants.all { variant ->
def outputFolder = new File("build/generated/ksp/${variant.name}/kotlin")
variant.addJavaSourceFoldersToModel(outputFolder)
android.sourceSets.getAt(variant.name).java {
srcDir(outputFolder)
}
}
} else if (project.extensions.findByType(com.android.build.gradle.AbstractAppExtension.class) != null) {
project.android.applicationVariants.all { variant ->
def outputFolder = new File("build/generated/ksp/${variant.name}/kotlin")
variant.addJavaSourceFoldersToModel(outputFolder)
android.sourceSets.getAt(variant.name).java {
srcDir(outputFolder)
}
}
}
}
}
def isNonStable = { String version ->
def stableKeyword = ['RELEASE', 'FINAL', 'GA'].any { it -> version.uppercase().contains(it) }
def regex = /^[0-9,.v-]+(-r)?$/
return !stableKeyword && !(version ==~ regex)
}
tasks.named("dependencyUpdates").configure {
// disallow release candidates as upgradable versions from stable versions
rejectVersionIf {
isNonStable(it.candidate.version) && !isNonStable(it.currentVersion)
}
}
================================================
FILE: epoxy-adapter/.gitignore
================================================
/build
================================================
FILE: epoxy-adapter/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply from: '../publishing.gradle'
android {
namespace 'com.airbnb.viewmodeladapter'
defaultConfig {
compileSdk rootProject.COMPILE_SDK_VERSION
minSdkVersion rootProject.MIN_SDK_VERSION
targetSdkVersion rootProject.TARGET_SDK_VERSION
consumerProguardFiles 'proguard-rules.pro'
}
testOptions.unitTests.includeAndroidResources = true
buildTypes.all { buildType ->
buildType.javaCompileOptions.annotationProcessorOptions.arguments =
[
logEpoxyTimings: "true"
]
}
}
kotlin {
jvmToolchain(11)
}
configurations.all { strategy ->
strategy.resolutionStrategy.force rootProject.deps.androidAnnotations, rootProject.deps.androidRecyclerView,
rootProject.deps.androidDesignLibrary, rootProject.deps.androidAppcompat, rootProject.deps.junit,
rootProject.deps.robolectric, rootProject.deps.mockito
}
dependencies {
implementation rootProject.deps.androidAppcompat
implementation rootProject.deps.androidAnnotations
implementation rootProject.deps.androidRecyclerView
implementation rootProject.deps.androidDesignLibrary
api project(':epoxy-annotations')
kapt project(':epoxy-processor')
kaptTest project(':epoxy-processor')
testImplementation rootProject.deps.junit
testImplementation rootProject.deps.robolectric
testImplementation rootProject.deps.mockito
testImplementation rootProject.deps.androidTestCore
}
================================================
FILE: epoxy-adapter/gradle.properties
================================================
POM_NAME=Epoxy
POM_ARTIFACT_ID=epoxy
POM_PACKAGING=jar
================================================
FILE: epoxy-adapter/lint.xml
================================================
================================================
FILE: epoxy-adapter/proguard-rules.pro
================================================
# The generated ControllerHelper classes are needed when using AutoModel annotations.
# Each ControllerHelper is looked up reflectively, so we need to make sure it is
# kept and its name not obfuscated so the reflective lookup works.
-keep class * extends com.airbnb.epoxy.EpoxyController { *; }
-keep class * extends com.airbnb.epoxy.ControllerHelper { *; }
-keepclasseswithmembernames class * { @com.airbnb.epoxy.AutoModel ; }
================================================
FILE: epoxy-adapter/src/main/AndroidManifest.xml
================================================
================================================
FILE: epoxy-adapter/src/main/java/com/airbnb/epoxy/ActivityRecyclerPool.kt
================================================
package com.airbnb.epoxy
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import androidx.core.view.ViewCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import androidx.recyclerview.widget.RecyclerView
import java.lang.ref.WeakReference
import java.util.ArrayList
internal class ActivityRecyclerPool {
/**
* Store one unique pool per activity. They are cleared out when activities are destroyed, so this
* only needs to hold pools for active activities.
*/
private val pools = ArrayList(5)
@JvmOverloads
fun getPool(
context: Context,
poolFactory: () -> RecyclerView.RecycledViewPool
): PoolReference {
val iterator = pools.iterator()
var poolToUse: PoolReference? = null
while (iterator.hasNext()) {
val poolReference = iterator.next()
when {
poolReference.context === context -> {
if (poolToUse != null) {
throw IllegalStateException("A pool was already found")
}
poolToUse = poolReference
// finish iterating to remove any old contexts
}
poolReference.context.isActivityDestroyed() -> {
// A pool from a different activity that was destroyed.
// Clear the pool references to allow the activity to be GC'd
poolReference.viewPool.clear()
iterator.remove()
}
}
}
if (poolToUse == null) {
poolToUse = PoolReference(context, poolFactory(), this)
context.lifecycle()?.addObserver(poolToUse)
pools.add(poolToUse)
}
return poolToUse
}
fun clearIfDestroyed(pool: PoolReference) {
if (pool.context.isActivityDestroyed()) {
pool.viewPool.clear()
pools.remove(pool)
}
}
private fun Context.lifecycle(): Lifecycle? {
if (this is LifecycleOwner) {
return lifecycle
}
if (this is ContextWrapper) {
return baseContext.lifecycle()
}
return null
}
}
internal class PoolReference(
context: Context,
val viewPool: RecyclerView.RecycledViewPool,
private val parent: ActivityRecyclerPool
) : LifecycleObserver {
private val contextReference: WeakReference = WeakReference(context)
val context: Context? get() = contextReference.get()
fun clearIfDestroyed() {
parent.clearIfDestroyed(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onContextDestroyed() {
clearIfDestroyed()
}
}
internal fun Context?.isActivityDestroyed(): Boolean {
if (this == null) {
return true
}
if (this !is Activity) {
return (this as? ContextWrapper)?.baseContext?.isActivityDestroyed() ?: false
}
if (isFinishing) {
return true
}
return if (Build.VERSION.SDK_INT >= 17) {
isDestroyed
} else {
// Use this as a proxy for being destroyed on older devices
!ViewCompat.isAttachedToWindow(window.decorView)
}
}
================================================
FILE: epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncEpoxyController.java
================================================
package com.airbnb.epoxy;
import android.os.Handler;
import static com.airbnb.epoxy.EpoxyAsyncUtil.MAIN_THREAD_HANDLER;
import static com.airbnb.epoxy.EpoxyAsyncUtil.getAsyncBackgroundHandler;
/**
* A subclass of {@link EpoxyController} that makes it easy to do model building and diffing in
* the background.
*
* See https://github.com/airbnb/epoxy/wiki/Epoxy-Controller#asynchronous-support
*/
public abstract class AsyncEpoxyController extends EpoxyController {
/**
* A new instance that does model building and diffing asynchronously.
*/
public AsyncEpoxyController() {
this(true);
}
/**
* @param enableAsync True to do model building and diffing asynchronously, false to do them
* both on the main thread.
*/
public AsyncEpoxyController(boolean enableAsync) {
this(enableAsync, enableAsync);
}
/**
* Individually control whether model building and diffing are done async or on the main thread.
*/
public AsyncEpoxyController(boolean enableAsyncModelBuilding, boolean enableAsyncDiffing) {
super(getHandler(enableAsyncModelBuilding), getHandler(enableAsyncDiffing));
}
private static Handler getHandler(boolean enableAsync) {
return enableAsync ? getAsyncBackgroundHandler() : MAIN_THREAD_HANDLER;
}
}
================================================
FILE: epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncEpoxyDiffer.java
================================================
package com.airbnb.epoxy;
import android.os.Handler;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
/**
* An adaptation of Google's {@link androidx.recyclerview.widget.AsyncListDiffer}
* that adds support for payloads in changes.
*
* Also adds support for canceling an in progress diff, and makes everything thread safe.
*/
class AsyncEpoxyDiffer {
interface ResultCallback {
void onResult(@NonNull DiffResult result);
}
private final Executor executor;
private final ResultCallback resultCallback;
private final ItemCallback> diffCallback;
private final GenerationTracker generationTracker = new GenerationTracker();
AsyncEpoxyDiffer(
@NonNull Handler handler,
@NonNull ResultCallback resultCallback,
@NonNull ItemCallback> diffCallback
) {
this.executor = new HandlerExecutor(handler);
this.resultCallback = resultCallback;
this.diffCallback = diffCallback;
}
@Nullable
private volatile List extends EpoxyModel>> list;
/**
* Non-null, unmodifiable version of list.
*
* Collections.emptyList when list is null, wrapped by Collections.unmodifiableList otherwise
*/
@NonNull
private volatile List extends EpoxyModel>> readOnlyList = Collections.emptyList();
/**
* Get the current List - any diffing to present this list has already been computed and
* dispatched via the ListUpdateCallback.
*
* If a null List, or no List has been submitted, an empty list will be returned.
*
* The returned list may not be mutated - mutations to content must be done through
* {@link #submitList(List)}.
*
* @return current List.
*/
@AnyThread
@NonNull
public List extends EpoxyModel>> getCurrentList() {
return readOnlyList;
}
/**
* Prevents any ongoing diff from dispatching results. Returns true if there was an ongoing
* diff to cancel, false otherwise.
*/
@SuppressWarnings("WeakerAccess")
@AnyThread
public boolean cancelDiff() {
return generationTracker.finishMaxGeneration();
}
/**
* @return True if a diff operation is in progress.
*/
@SuppressWarnings("WeakerAccess")
@AnyThread
public boolean isDiffInProgress() {
return generationTracker.hasUnfinishedGeneration();
}
/**
* Set the current list without performing any diffing. Cancels any diff in progress.
*
* This can be used if you notified a change to the adapter manually and need this list to be
* synced.
*/
@AnyThread
public synchronized boolean forceListOverride(@Nullable List> newList) {
// We need to make sure that generation changes and list updates are synchronized
final boolean interruptedDiff = cancelDiff();
int generation = generationTracker.incrementAndGetNextScheduled();
tryLatchList(newList, generation);
return interruptedDiff;
}
/**
* Set a new List representing your latest data.
*
* A diff will be computed between this list and the last list set. If this has not previously
* been called then an empty list is used as the previous list.
*
* The diff computation will be done on the thread given by the handler in the constructor.
* When the diff is done it will be applied (dispatched to the result callback),
* and the new List will be swapped in.
*/
@AnyThread
@SuppressWarnings("WeakerAccess")
public void submitList(@Nullable final List extends EpoxyModel>> newList) {
final int runGeneration;
@Nullable final List extends EpoxyModel>> previousList;
synchronized (this) {
// Incrementing generation means any currently-running diffs are discarded when they finish
// We synchronize to guarantee list object and generation number are in sync
runGeneration = generationTracker.incrementAndGetNextScheduled();
previousList = list;
}
if (newList == previousList) {
// nothing to do
onRunCompleted(runGeneration, newList, DiffResult.noOp(previousList));
return;
}
if (newList == null || newList.isEmpty()) {
// fast simple clear all
DiffResult result = null;
if (previousList != null && !previousList.isEmpty()) {
result = DiffResult.clear(previousList);
}
onRunCompleted(runGeneration, null, result);
return;
}
if (previousList == null || previousList.isEmpty()) {
// fast simple first insert
onRunCompleted(runGeneration, newList, DiffResult.inserted(newList));
return;
}
final DiffCallback wrappedCallback = new DiffCallback(previousList, newList, diffCallback);
executor.execute(new Runnable() {
@Override
public void run() {
DiffUtil.DiffResult result = DiffUtil.calculateDiff(wrappedCallback);
onRunCompleted(runGeneration, newList, DiffResult.diff(previousList, newList, result));
}
});
}
private void onRunCompleted(
final int runGeneration,
@Nullable final List extends EpoxyModel>> newList,
@Nullable final DiffResult result
) {
// We use an asynchronous handler so that the Runnable can be posted directly back to the main
// thread without waiting on view invalidation synchronization.
MainThreadExecutor.ASYNC_INSTANCE.execute(new Runnable() {
@Override
public void run() {
final boolean dispatchResult = tryLatchList(newList, runGeneration);
if (result != null && dispatchResult) {
resultCallback.onResult(result);
}
}
});
}
/**
* Marks the generation as done, and updates the list if the generation is the most recent.
*
* @return True if the given generation is the most recent, in which case the given list was
* set. False if the generation is old and the list was ignored.
*/
@AnyThread
private synchronized boolean tryLatchList(@Nullable List extends EpoxyModel>> newList,
int runGeneration) {
if (generationTracker.finishGeneration(runGeneration)) {
list = newList;
if (newList == null) {
readOnlyList = Collections.emptyList();
} else {
readOnlyList = Collections.unmodifiableList(newList);
}
return true;
}
return false;
}
/**
* The concept of a "generation" is used to associate a diff result with a point in time when
* it was created. This allows us to handle list updates concurrently, and ignore outdated diffs.
*
* We track the highest start generation, and the highest finished generation, and these must
* be kept in sync, so all access to this class is synchronized.
*
* The general synchronization strategy for this class is that when a generation number
* is queried that action must be synchronized with accessing the current list, so that the
* generation number is synced with the list state at the time it was created.
*/
private static class GenerationTracker {
// Max generation of currently scheduled runnable
private volatile int maxScheduledGeneration;
private volatile int maxFinishedGeneration;
synchronized int incrementAndGetNextScheduled() {
return ++maxScheduledGeneration;
}
synchronized boolean finishMaxGeneration() {
boolean isInterrupting = hasUnfinishedGeneration();
maxFinishedGeneration = maxScheduledGeneration;
return isInterrupting;
}
synchronized boolean hasUnfinishedGeneration() {
return maxScheduledGeneration > maxFinishedGeneration;
}
synchronized boolean finishGeneration(int runGeneration) {
boolean isLatestGeneration =
maxScheduledGeneration == runGeneration && runGeneration > maxFinishedGeneration;
if (isLatestGeneration) {
maxFinishedGeneration = runGeneration;
}
return isLatestGeneration;
}
}
private static class DiffCallback extends DiffUtil.Callback {
final List extends EpoxyModel>> oldList;
final List extends EpoxyModel>> newList;
private final ItemCallback> diffCallback;
DiffCallback(List extends EpoxyModel>> oldList, List extends EpoxyModel>> newList,
ItemCallback> diffCallback) {
this.oldList = oldList;
this.newList = newList;
this.diffCallback = diffCallback;
}
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return diffCallback.areItemsTheSame(
oldList.get(oldItemPosition),
newList.get(newItemPosition)
);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return diffCallback.areContentsTheSame(
oldList.get(oldItemPosition),
newList.get(newItemPosition)
);
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return diffCallback.getChangePayload(
oldList.get(oldItemPosition),
newList.get(newItemPosition)
);
}
}
}
================================================
FILE: epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java
================================================
package com.airbnb.epoxy;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import com.airbnb.epoxy.stickyheader.StickyHeaderCallbacks;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.List;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
import androidx.recyclerview.widget.RecyclerView;
public abstract class BaseEpoxyAdapter
extends RecyclerView.Adapter
implements StickyHeaderCallbacks {
private static final String SAVED_STATE_ARG_VIEW_HOLDERS = "saved_state_view_holders";
private int spanCount = 1;
private final ViewTypeManager viewTypeManager = new ViewTypeManager();
/**
* Keeps track of view holders that are currently bound so we can save their state in {@link
* #onSaveInstanceState(Bundle)}.
*/
private final BoundViewHolders boundViewHolders = new BoundViewHolders();
private ViewHolderState viewHolderState = new ViewHolderState();
private final SpanSizeLookup spanSizeLookup = new SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
try {
return getModelForPosition(position)
.spanSize(spanCount, position, getItemCount());
} catch (IndexOutOfBoundsException e) {
// There seems to be a GridLayoutManager bug where when the user is in accessibility mode
// it incorrectly uses an outdated view position
// when calling this method. This crashes when a view is animating out, when it is
// removed from the adapter but technically still added
// to the layout. We've posted a bug report and hopefully can update when the support
// library fixes this
// TODO: (eli_hart 8/23/16) Figure out if this has been fixed in new support library
onExceptionSwallowed(e);
return 1;
}
}
};
public BaseEpoxyAdapter() {
// Defaults to stable ids since view models generate unique ids. Set this to false in the
// subclass if you don't want to support it
setHasStableIds(true);
spanSizeLookup.setSpanIndexCacheEnabled(true);
}
/**
* This is called when recoverable exceptions happen at runtime. They can be ignored and Epoxy
* will recover, but you can override this to be aware of when they happen.
*/
protected void onExceptionSwallowed(RuntimeException exception) {
}
@Override
public int getItemCount() {
return getCurrentModels().size();
}
/** Return the models currently being used by the adapter to populate the recyclerview. */
abstract List extends EpoxyModel>> getCurrentModels();
public boolean isEmpty() {
return getCurrentModels().isEmpty();
}
@Override
public long getItemId(int position) {
// This does not call getModelForPosition so that we don't use the id of the empty model when
// hidden,
// so that the id stays constant when gone vs shown
return getCurrentModels().get(position).id();
}
@Override
public int getItemViewType(int position) {
return viewTypeManager.getViewTypeAndRememberModel(getModelForPosition(position));
}
@Override
public EpoxyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
EpoxyModel> model = viewTypeManager.getModelForViewType(this, viewType);
View view = model.buildView(parent);
return new EpoxyViewHolder(parent, view, model.shouldSaveViewState());
}
@Override
public void onBindViewHolder(EpoxyViewHolder holder, int position) {
onBindViewHolder(holder, position, Collections.emptyList());
}
@Override
public void onBindViewHolder(EpoxyViewHolder holder, int position, List