├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── tumblr │ │ └── example │ │ ├── App.java │ │ ├── MainActivity.java │ │ ├── PrimitiveAdapter.java │ │ ├── binder │ │ ├── ColorNameToastBinder.java │ │ ├── HeaderBinder.java │ │ ├── PaletteColorBinder.java │ │ └── TextPrimitiveBinder.java │ │ ├── binderlist │ │ ├── ColorNamePrimitiveItemBinder.java │ │ ├── HeaderPrimitiveItemBinder.java │ │ └── PaletteItemBinder.java │ │ ├── dagger │ │ ├── AppComponent.java │ │ ├── PerActivity.java │ │ ├── key │ │ │ ├── PrimitiveCreatorKey.java │ │ │ └── PrimitiveItemBinderKey.java │ │ └── module │ │ │ ├── ActionListenerModule.java │ │ │ ├── ActivityBindingModule.java │ │ │ ├── ItemBinderModule.java │ │ │ └── ViewHolderCreatorModule.java │ │ ├── model │ │ ├── ColorNamePrimitive.java │ │ ├── Palette.java │ │ ├── Primitive.java │ │ └── TextPrimitive.java │ │ ├── viewholder │ │ ├── ColorPrimitiveViewHolder.java │ │ ├── HeaderViewHolder.java │ │ ├── PrimitiveViewHolder.java │ │ └── TextPrimitiveViewHolder.java │ │ └── viewholdercreator │ │ ├── ColorPrimitiveViewHolderCreator.java │ │ ├── HeaderViewHolderCreator.java │ │ └── TextPrimitiveViewHolderCreator.java │ └── res │ ├── layout │ ├── activity_main.xml │ ├── item_color.xml │ ├── item_header.xml │ └── item_text.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── graywater ├── .gitignore ├── build.gradle ├── checkstyle-suppressions.xml ├── checkstyle.xml ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── tumblr │ │ └── graywater │ │ └── GraywaterAdapter.java │ └── test │ ├── java │ └── com │ │ └── tumblr │ │ └── graywater │ │ └── GraywaterAdapterTest.java │ └── resources │ └── robolectric.properties └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | graywater -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We want to make contributing to Graywater as easy and transparent as possible. If you run into problems, please open an issue. We also actively welcome pull requests. 4 | 5 | ## Pull Requests 6 | 7 | 1. Fork the repo and create your branch from `master`. 8 | 2. If you've added code that should be tested, add tests. 9 | 3. If you've changed APIs, update the documentation. 10 | 4. Ensure the test suite passes. 11 | 5. If you haven't already, complete the Contributor License Agreement ("CLA"). 12 | 13 | ## Contributor License Agreement ("CLA") 14 | 15 | In order to accept your pull request, we need you to submit a CLA. 16 | 17 | Complete your CLA [here](http://static.tumblr.com/zyubucd/GaTngbrpr/tumblr_corporate_contributor_license_agreement_v1__10-7-14.pdf) (a more integrated web form is coming soon). 18 | 19 | ## License 20 | 21 | By contributing to Graywater you agree that your contributions will be licensed under its Apache 2.0 license. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Tumblr, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Graywater: an android library for performant lists 2 | 3 | Graywater is a [`RecyclerView`](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html) adapter that facilitates the performant decomposition of complex and varied list items. It does this by mapping large data models to multiple viewholders, splitting the work needed to create a complex list item over multiple frames. 4 | 5 | The concept is based off of [Facebook's post on a faster news feed](https://code.facebook.com/posts/879498888759525/fast-rendering-news-feed-on-android/) and [Components for Android](https://code.facebook.com/posts/531104390396423/components-for-android-a-declarative-framework-for-efficient-uis/), which have been realized as [Litho](http://fblitho.com). 6 | 7 | Tumblr developed Graywater to improve scroll performance, reduce memory usage, and lay the foundation for a more modular codebase. 8 | 9 | The name "Graywater" comes from [the process of recycling water](https://en.wikipedia.org/wiki/Greywater). 10 | 11 | * [What is it?](#what-is-it) 12 | * [How do you use it?](#how-do-you-use-it) 13 | * [How does it work?](#how-does-it-work) 14 | * [Other features](#other-features) 15 | * [An addendum on binders and generics](#an-addendum-on-binders-and-generics) 16 | 17 | ## What is it? 18 | 19 | An adapter basically takes a list of models (of type `T`) and maps them to a list of viewholders (of type `VH extends RecyclerView.ViewHolder`). 20 | 21 | One naive solution is to map models directly to viewholders. For example, a list of "posts" can have a viewholder for each post. But this architecture quickly becomes slow and unwieldy if there is either a large variety of posts or if individual posts are complex. 22 | 23 | So to improve performance, the parts of a post that are offscreen can be recycled. 24 | 25 | ``` 26 | model views 27 | +---------+ +------+ 28 | | | | head | <------- does not exist 29 | | | +------+ <------------+ 30 | | item #1 | | body | <---+ | 31 | | | +------+ | | 32 | | | | foot | | | 33 | +---------+ +------+ | | 34 | screen view hierarchy 35 | +---------+ +------+ | | 36 | | | | head | | | 37 | | | +------+ | | 38 | | item #2 | | body | <---+ | 39 | | | +------+ <------------+ 40 | | | | body | <------- does not exist 41 | +---------+ +------+ 42 | ``` 43 | 44 | Due to Tumblr's needs, there are additional features that help improve performance and reduce memory usage: 45 | 46 | * Viewholders are shared between models of the same and different types, _e.g. a body viewholder can be shared between a item #1 and item #2_. 47 | * Models can have multiple viewholders of the same type, _e.g. an item can have an unlimited number of body viewholders_. 48 | 49 | This results in a minimal number of viewholders to maximize cache effectiveness and reduce memory pressure. 50 | 51 | In order to accomplish this, we introduce the concept of a **Binder**, which takes a model (`T`) and binds it to a viewholder (`VH`). 52 | 53 | ``` 54 | +-------+ +--------+ +------------+ 55 | | Model | --> | Binder | --> | ViewHolder | 56 | +-------+ +--------+ +------------+ 57 | ``` 58 | 59 | We no longer desire the one-to-one relationship between models and viewholders that, because monolithic models result in monolithic viewholders. For example, a video post (`VideoPost`) used to have a corresponding `VideoPostViewHolder`. Instead, we want `VideoPost` to be composed of a header, body, and footer. 60 | 61 | ``` 62 | +--------+ +------------+ 63 | /--> | Binder | --> | ViewHolder | 64 | +-------+ +---+ / +--------+ +------------+ 65 | | Model | --> | ? | *----> | Binder | --> | ViewHolder | 66 | +-------+ +---+ \ +--------+ +------------+ 67 | \--> | Binder | --> | ViewHolder | 68 | +--------+ +------------+ 69 | ``` 70 | 71 | To manage this relationship, we introduce the concept of an **ItemBinder**, which aggregates the binders needed to display a post. It takes a model (`T`) and returns a list of binders, each of which bind the model to a specific viewholder. 72 | 73 | ``` 74 | +------------+ 75 | /-----> | ItemBinder | 76 | / +------------+ 77 | / v 78 | / +--------+ +------------+ 79 | / /----> | Binder | --> | ViewHolder | 80 | +-------+ / +--------+ +------------+ 81 | | Model | *------> | Binder | --> | ViewHolder | 82 | +-------+ \ +--------+ +------------+ 83 | \----> | Binder | --> | ViewHolder | 84 | +--------+ +------------+ 85 | ``` 86 | 87 | * `ItemBinder` takes a model `T` and maps it to a list of binders of type `Binder`. 88 | * `Binder` takes a model of type `T` and maps it to a `ViewHolder` of type `VH` 89 | 90 | A minor design point is that `RecyclerView.Adapter#onCreate()` creates the viewholders, so some sort of mechanism for creating viewholders is necessary. This is where **ViewHolderCreator** comes in - it is a model-independent way of creating viewholders (in other libraries with a one-to-one relationship between models and viewholders, this code would live in the model - e.g. [Epoxy](https://github.com/airbnb/epoxy#epoxy-models)). 91 | 92 | ``` 93 | +------------+ 94 | /-----> | ItemBinder | 95 | / +------------+ 96 | / v 97 | / +--------+ +------------+ +-------------------+ 98 | / /----> | Binder | --> | ViewHolder | <-- | ViewHolderCreator | 99 | +-------+ / +--------+ +------------+ +-------------------+ 100 | | Model | *------> | Binder | --> | ViewHolder | <-- | ViewHolderCreator | 101 | +-------+ \ +--------+ +------------+ +-------------------+ 102 | \----> | Binder | --> | ViewHolder | <-- | ViewHolderCreator | 103 | +--------+ +------------+ +-------------------+ 104 | ``` 105 | 106 | ### Dependency Injection with Dagger 2 Map Multibindings 107 | 108 | For Graywater to know about the ItemBinders and ViewHolderCreators, each of them needs to be registered when the adapter is created. When there are a substantial number of both, there can be a significant impact on the time it takes to initialize the adapter. 109 | 110 | One solution is to use [Dagger 2 map multibindings](https://google.github.io/dagger/multibindings#map-multibindings). This allows you to use the full power of dependency injection to control which binders a given screen will support, as well as the ability to inject different versions of the same binder on different screens to facilitate screen-dependent behavior. 111 | 112 | ``` 113 | +------------+ +----------------+ 114 | /-----> | ItemBinder | <-------------------- | Dagger 2 Maps | 115 | / +------------+ +----------------+ 116 | / v v 117 | / +--------+ +------------+ +-------------------+ 118 | / /----> | Binder | --> | ViewHolder | <-- | ViewHolderCreator | 119 | +-------+ / +--------+ +------------+ +-------------------+ 120 | | Model | *------> | Binder | --> | ViewHolder | <-- | ViewHolderCreator | 121 | +-------+ \ +--------+ +------------+ +-------------------+ 122 | \----> | Binder | --> | ViewHolder | <-- | ViewHolderCreator | 123 | +--------+ +------------+ +-------------------+ 124 | ``` 125 | 126 | But using Dagger 2 by itself does not improve startup time, because the maps are created at injection time, which requires all the binders to also be created. This can be somewhat alleviated with `Lazy`, but another benefit of Dagger 2 is the automatic support for `Map>`. When applied to `ItemBinders`, this allows each `ItemBinder` to be constructed on demand. 127 | 128 | _Note that Graywater does not have built-in support for Dagger 2._ 129 | 130 | ### Lazy Loading Binders 131 | 132 | Normally, when an item is added to the adapter, the corresponding `ItemBinder` is loaded as well as all the necessary `Binder` classes. 133 | 134 | ``` 135 | Binders ItemBinders Items Screen 136 | +--------+ +------------+ +-----------+ +--------+ 137 | | Photo | -------- | | /- | TextPost | | Header | 138 | +--------+ /---- | Photo Post | -\ / +-----------+ +--------+ 139 | | Footer | --x /--- | | \----- | PhotoPost | | | 140 | +--------+ x +------------+ / +-----------+ | | 141 | | Header | --x \--- | | --/ /- | TextPost | | Text | 142 | +--------+ \---- | Text Post | / +-----------+ | | 143 | | Text | -------- | | ----/ | | 144 | +--------+ +------------+ +--------+ 145 | ``` 146 | 147 | But on-screen, only the first item is visible, and out of the first item, only two components are visible. So in the above example, there is no need to load the "Footer" binder. This is what `List>` facilitates. 148 | 149 | ``` 150 | Binders ItemBinders Items Screen 151 | +--------+ +------------+ +-----------+ +--------+ 152 | | Photo | | | /-- | TextPost | -x--- | Header | 153 | +--------+ | Photo Post | / +-----------+ \ +--------+ 154 | | Footer | | | / | PhotoPost | \- | | 155 | +--------+ +------------+ / +-----------+ | | 156 | | Header | ---\ | | -/ | TextPost | | Text | 157 | +--------+ \--- | Text Post | +-----------+ | | 158 | | Text | -------- | | | | 159 | +--------+ +------------+ +--------+ 160 | ``` 161 | 162 | This is very useful for improving initialization performance when loading long cached lists by deferring binder creation until the binder is nearly on screen. 163 | 164 | ## How do you use it? 165 | 166 | Graywater relies heavily on generics for type safety - here are the major type parameters: 167 | 168 | * `T` is the base model type. 169 | * `VH` is the base viewholder type. 170 | * `MT` is the type of the model type (e.g. `Class`). 171 | 172 | Although this may seem overly generic, it is convenient if your base model or viewholder type has methods you need to access. 173 | 174 | Add a model that subclasses `T`. 175 | 176 | ```java 177 | class Text { 178 | String text; 179 | } 180 | ``` 181 | 182 | Create the viewholder(s). 183 | 184 | ```java 185 | class TextViewHolder extends RecyclerView.ViewHolder { 186 | 187 | TextView textView; 188 | 189 | public TextViewHolder(View view) { 190 | super(view); 191 | textView = (TextView) view.findViewById(R.id.text); 192 | } 193 | } 194 | ``` 195 | 196 | Create the corresponding `ViewHolderCreator` implementations. 197 | 198 | ```java 199 | class TextViewHolderCreator implements GraywaterAdapter.ViewHolderCreator { 200 | 201 | public TextViewHolder create(final ViewGroup parent) { 202 | return new TextViewHolder(GraywaterAdapter.inflate(parent, R.layout.item_text)); 203 | } 204 | 205 | public int getViewType() { 206 | return R.layout.item_text; 207 | } 208 | } 209 | ``` 210 | 211 | Create the `Binder` implementations for each `ViewHolder`. 212 | 213 | ```java 214 | class TextBinder implements GraywaterAdapter.Binder { 215 | 216 | public Class getViewHolderType() { 217 | return TextViewHolder.class; 218 | } 219 | 220 | public void prepare(final Text model, 221 | final List> binders, 222 | final int binderIndex) { 223 | 224 | } 225 | 226 | public void bind(final Text model, 227 | final TextViewHolder holder, 228 | final List> binders, 229 | final int binderIndex, 230 | final GraywaterAdapter.ActionListener actionListener) { 231 | holder.textView.setText(model.text); 232 | } 233 | 234 | public void unbind(final TextViewHolder holder) { 235 | holder.textView.setText(null); 236 | } 237 | } 238 | ``` 239 | 240 | Create the `ItemBinder` that returns the list of binders for the model. 241 | 242 | ```java 243 | class TextItemBinder implements GraywaterAdapter.ItemBinder { 244 | 245 | TextBinder textBinder; 246 | 247 | public TextItemBinder(TextBinder textBinder) { 248 | this.textBinder = textBinder; 249 | } 250 | 251 | public List> getBinderList( 252 | final Text model, 253 | final int position) { 254 | return new ArrayList>() {{ 255 | add(textBinder); 256 | add(textBinder); 257 | }}; 258 | } 259 | } 260 | ``` 261 | 262 | Lastly, subclass `GraywaterAdapter` and register the created classes! 263 | 264 | ```java 265 | private static class TextAdapter extends GraywaterAdapter> { 266 | 267 | public TextAdapter() { 268 | register(new TextViewHolderCreator(), TextViewHolder.class); 269 | 270 | final TextBinder textBinder = new TextBinder(); 271 | 272 | register(String.class, new TextItemBinder(textBinder), null); 273 | } 274 | 275 | @Override 276 | protected Class getModelType(final Text model) { 277 | return model.getClass(); 278 | } 279 | } 280 | ``` 281 | 282 | You can then add items using `GraywaterAdapter.add()` or remove them with `GraywaterAdapter.remove()`. Note that `getItemCount()` will return the number of viewholders, not the number of model objects in your list. `getModelType(MT)` will generally have the example implementation, but it may be useful to have a custom implementation if subtypes have different definitions across types, or if you need a "default" type. 283 | 284 | In example code, an adapter is created that repeats each item in the list once. 285 | 286 | ## How does it work? 287 | 288 | At its core, Graywater maps models to viewholders, which basically means it is a just a dictionary. These are the fields used in a dictionary-like way: 289 | 290 | * `List mItems` - the list of items (or a map of position to item) 291 | * `Map, ViewHolderCreator> mViewHolderCreatorMap` - the map of viewholder class to `ViewHolderCreator`. 292 | * `Map> mItemBinderMap` - the map of `MT` (model type) to `ItemBinder` 293 | * `Map> mActionListenerMap` - the map of `MT` (model type) to `ActionListener` 294 | 295 | So in `add()`, the new model is added to the list of items. In `register()`, the parameters are added to the respective map. 296 | 297 | A simple optimization is to cache the ItemBinders. This is done by `binderListCache`, which is of type `List>>`. Every time `add()` is called, `getBinderList()` is called and the return value is added to the cache. 298 | 299 | What is `MT`? 300 | 301 | ```java 302 | protected abstract MT getModelType(T model); 303 | ``` 304 | 305 | Instead of automatically using the class of the model as the model's type, it can be anything (preferably a similar property of the model). 306 | 307 | Note that `RecyclerView.Adapter` has these methods: 308 | 309 | ```java 310 | abstract class Adapter { 311 | abstract VH onCreateViewHolder(ViewGroup parent, int viewType); 312 | abstract void onBindViewHolder(VH holder, int position); 313 | int getItemViewType(int position); 314 | abstract int getItemCount(); 315 | } 316 | ``` 317 | 318 | It is important to note that `position` in the above methods is the _viewholder_ position, not the _model_ position. This distinction is extremely important, because when we are given the _viewholder_ position when we need the _model_ position. 319 | 320 | For now, we assume that `viewType` has a one-to-one correspondence to the viewholder class. 321 | 322 | Here is a visualization of the model and viewholder positions: 323 | 324 | ``` 325 | model viewholder 326 | position position 327 | 328 | +-----+ +-----+ 329 | | | | 0 | 330 | | | +-----+ 331 | | | | 1 | 332 | | 0 | +-----+ 333 | | | | 2 | 334 | | | +-----+ 335 | | | | 3 | 336 | +-----+ +-----+ 337 | 338 | +-----+ +-----+ 339 | | | | 4 | 340 | | 1 | +-----+ 341 | | | | 5 | 342 | +-----+ +-----+ 343 | ``` 344 | 345 | If we are given a viewholder position of `5`, we need to arrive at the model position of `1`, that way we can grab the model from the backing data store. 346 | 347 | The way to do this is to iterate through the models, going to the corresponding `ItemBinder` and accumulating the size of the list that is returned. Unfortunately, this is slow. 348 | 349 | But in order to make it fast, we need to cache a lot of intermediary state. 350 | On `add()`, we compute these two caches, `viewHolderToItemPosition` and `itemPositionToFirstViewHolderPosition`. Note that the code uses _item_ to refer to the _model_ position. 351 | 352 | ``` 353 | model viewholder viewHolderToItemPos itemPosToFirstViewHolderPos 354 | position position 355 | 356 | +-----+ +-----+ 357 | | | | 0 | { 0, 0 } 358 | | | +-----+ 359 | | | | 1 | { 1, 0 } 360 | | 0 | +-----+ { 0, 0 } 361 | | | | 2 | { 2, 0 } 362 | | | +-----+ 363 | | | | 3 | { 3, 0 } 364 | +-----+ +-----+ 365 | 366 | +-----+ +-----+ 367 | | | | 4 | { 4, 1 } 368 | | 1 | +-----+ { 1, 4 } 369 | | | | 5 | { 5, 1 } 370 | +-----+ +-----+ 371 | ``` 372 | 373 | `viewHolderToItemPositionCache` is also used for `getItemCount()`. 374 | 375 | `itemPositionToFirstViewHolderPosition` is primarily used for one purpose: to determine the position of the viewholder and associated binder in the list of viewholders _for a given model_. In the above example, the viewholder at position `5` is the 2nd viewholder for the 2nd model. This is important when there is more than one instance of a viewholder for a model, such as reblog comments. 376 | 377 | `getItemViewType()` works by tracking the registered `ViewHolderCreators`, which have this interface: 378 | 379 | ```java 380 | interface ViewHolderCreator { 381 | RecyclerView.ViewHolder create(ViewGroup parent); 382 | int getViewType(); 383 | } 384 | ``` 385 | 386 | When a new `ViewHolderCreator` is registered, it is added to `viewHolderCreatorList`, which is of type `SparseArray>`, and associates the `viewType` with the correct class. The class is then associated with the `ViewHolderCreator` via `viewHolderCreatorMap`, which is of type `Map, ViewHolderCreator>`. 387 | 388 | ``` 389 | viewHolderCreatorList viewHolderCreatorMap 390 | +------------------------------+ +---------------------------------------+ 391 | | viewtype -> ViewHolder.class | | ViewHolder.class -> ViewHolderCreator | 392 | +==============================+ +=======================================+ 393 | | 8324 -> Header.class | | Header.class -> HeaderCreator | 394 | +------------------------------+ +---------------------------------------+ 395 | | 9802 -> Body.class | | Body.class -> BodyCreator | 396 | +------------------------------+ +---------------------------------------+ 397 | | 2383 -> Footer.class | | Footer.class -> FooterCreator | 398 | +------------------------------+ +---------------------------------------+ 399 | ``` 400 | 401 | To implement `getItemViewType()` So when given a _viewholder_ position, 402 | 403 | 1. `viewHolderToItemPos` is used to retrieve the model position. 404 | 2. `binderListCache` and the model position is used to get the list of binders. 405 | 3. `itemPosToFirstViewHolderPos` is used to retrieve the position of the first viewholder. 406 | 4. The binder position in the list of binders is computed. 407 | 5. The correct binder for the viewholder position is retrieved. 408 | 6. `viewHolderCreatorMap` is passed the `Binder.getViewHolderType()` to get the `ViewHolderCreator`. 409 | 7. `ViewHolderCreator.getViewType()` is called to get the `viewType`. 410 | 411 | `onCreateViewHolder(ViewGroup parent, int viewType)` is quite a bit simpler: 412 | 413 | ```java 414 | return (VH) viewHolderCreatorMap.get(getViewHolderClass(viewType)).create(parent); 415 | ``` 416 | 417 | 1. `viewHolderCreatorList` is used to get the class from the `viewType` 418 | 2. `viewHolderCreatorMap` is used to get the `ViewHolderCreator` 419 | 3. The `ViewHolderCreator` creates the new viewholder. 420 | 421 | `onBindViewHolder(VH holder, int viewHolderPosition)` is implemented by following the first 5 steps of `getItemViewType()`, and then calling `Binder.bind()` with the model and the viewholder. 422 | 423 | ## Other features 424 | 425 | In `bind()`, the adapter looks ahead to the next `numViewHoldersToPrepare()` viewholders (default 3) and calls `Binder.prepare()` on them. Note that it does not call `prepare()` more than once, unless `unbind()` is called. This state is stored in `viewHolderPreparedCache` which stores the indices of viewholders that have been prepared. 426 | 427 | This also works in both directions of the `RecyclerView`. It checks the order of `bind()` operations to determine which direction to prepare viewholders in. 428 | 429 | `ActionListener` is a bit of an experimental feature to avoid creating extra `ClickListener` objects on every `bind()` call. 430 | 431 | ## An addendum on binders and generics 432 | 433 | `ItemBinder#getBinderList()` has a somewhat complex return type: 434 | 435 | ```java 436 | List> getBinderList(@NonNull T model, int position) 437 | ``` 438 | 439 | In particular, `Binder` is quite confusing. 440 | 441 | When you write your binder, try to parameterize the binder with the least-restrictive model it can take and the most restrictive viewholder it can bind to. 442 | 443 | If these were your type hierarchies: 444 | 445 | ``` 446 | Model Type ViewHolder Type 447 | Hierarchy Hierarchy 448 | 449 | A 1 450 | / \ <- Binder -> / \ 451 | B C 2 3 452 | ``` 453 | 454 | * A binder should be written to take `A` if possible, `B` or `C` if necessary. 455 | * A binder can only be written to take `2` or `3` (note that `1` should never be registered, because then it is ambiguous). 456 | 457 | This can be illustrated with an example 458 | 459 | ``` 460 | Post BaseViewHolder 461 | / \ <- Binder -> / \ 462 | Text Photo Header Body 463 | ``` 464 | 465 | If every post has a header, it makes sense to have 466 | 467 | * `HeaderBinder extends Binder` 468 | * `PhotoBinder extends Binder` 469 | * `TextBinder extends Binder` 470 | 471 | That way `HeaderBinder` can take a `Text` or a `Photo`, while `PhotoBinder` won't ever take a `Text`. 472 | 473 | So what does the `ItemBinder` look like? 474 | 475 | * `TextItemBinder extends ItemBinder` 476 | - `HeaderBinder` 477 | - `TextBinder` 478 | * `PhotoItemBinder extends ItemBinder` 479 | - `HeaderBinder` 480 | - `PhotoBinder` 481 | 482 | You can see that `HeaderBinder` binds `Post`, which is a superclass of `Text`, while `TextBinder` binds to `Body`, which is a subclass of `BaseViewHolder`. 483 | 484 | When registering: 485 | 486 | * `register(Text.class, textItemBinder)` 487 | * `register(Photo.class, photoItemBInder)` 488 | 489 | assuming the class is the model type. 490 | 491 | The adapter should be of type `GraywaterAdapter`, the superclasses for all the types used. 492 | 493 | ## Contact 494 | 495 | * [Eric Leong](mailto:ericleong@tumblr.com) 496 | 497 | ## License 498 | 499 | Copyright 2017 Tumblr, Inc. 500 | 501 | Licensed under the Apache License, Version 2.0 (the “License”); you may not use 502 | this file except in compliance with the License. You may obtain a copy of the 503 | License at [apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). 504 | 505 | > Unless required by applicable law or agreed to in writing, software 506 | > distributed under the License is distributed on an “AS IS” BASIS, WITHOUT 507 | > WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 508 | > License for the specific language governing permissions and limitations under 509 | > the License. 510 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion '26.0.2' 6 | 7 | defaultConfig { 8 | applicationId "com.tumblr.graywater" 9 | minSdkVersion 16 10 | targetSdkVersion 26 11 | versionCode 2 12 | versionName "0.0.2" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(dir: 'libs', include: ['*.jar']) 24 | 25 | implementation project(path: ':graywater') 26 | 27 | implementation 'com.android.support:appcompat-v7:26.1.0' 28 | implementation 'com.android.support:recyclerview-v7:26.1.0' 29 | implementation('com.google.dagger:dagger:2.13') { 30 | exclude group: 'com.google.code.findbugs', module: 'jsr305' 31 | } 32 | implementation('com.google.dagger:dagger-android:2.13') { 33 | exclude group: 'com.google.code.findbugs', module: 'jsr305' 34 | } 35 | implementation('com.google.dagger:dagger-android-support:2.13') { 36 | exclude group: 'com.google.code.findbugs', module: 'jsr305' 37 | } 38 | 39 | annotationProcessor 'com.google.dagger:dagger-compiler:2.13' 40 | annotationProcessor 'com.google.dagger:dagger-android-processor:2.13' 41 | 42 | testImplementation 'junit:junit:4.12' 43 | } 44 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/ericleong/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/App.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example; 2 | 3 | import com.tumblr.example.dagger.DaggerAppComponent; 4 | import dagger.android.AndroidInjector; 5 | import dagger.android.DaggerApplication; 6 | 7 | /** 8 | * Created by ericleong on 12/6/17. 9 | */ 10 | public class App extends DaggerApplication { 11 | @Override 12 | protected AndroidInjector applicationInjector() { 13 | return DaggerAppComponent.builder().create(this); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.widget.DefaultItemAnimator; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.support.v7.widget.RecyclerView; 7 | import com.tumblr.example.model.ColorNamePrimitive; 8 | import com.tumblr.example.model.Palette; 9 | import com.tumblr.example.model.Primitive; 10 | import dagger.android.support.DaggerAppCompatActivity; 11 | 12 | import javax.inject.Inject; 13 | 14 | public class MainActivity extends DaggerAppCompatActivity { 15 | 16 | @Inject 17 | PrimitiveAdapter mPrimitiveAdapter; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_main); 23 | 24 | final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list); 25 | 26 | if (recyclerView != null) { 27 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 28 | recyclerView.setItemAnimator(new DefaultItemAnimator()); 29 | 30 | // A header has nothing special 31 | mPrimitiveAdapter.add(new Primitive.Header()); 32 | 33 | // Reds 34 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.red_base_variant_0, "dark red")); 35 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.red_base_variant_1, "red")); 36 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.red_base_variant_2, "bright red")); 37 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.red_base_variant_3, "shy red")); 38 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.red_base_variant_4, "embarrassed red")); 39 | mPrimitiveAdapter.add(new Palette("Red Palette", R.color.red_base_variant_0, R.color.red_base_variant_2, R.color.red_base_variant_4)); 40 | 41 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.yellow_base_variant_0, "dark yellow")); 42 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.yellow_base_variant_1, "yellow")); 43 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.yellow_base_variant_2, "bright yellow")); 44 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.yellow_base_variant_3, "shy yellow")); 45 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.yellow_base_variant_4, "embarrassed yellow")); 46 | mPrimitiveAdapter.add(new Palette("Yellow Palette", 47 | R.color.yellow_base_variant_0, R.color.yellow_base_variant_2, R.color.yellow_base_variant_4)); 48 | 49 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.green_base_variant_0, "dark green")); 50 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.green_base_variant_1, "green")); 51 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.green_base_variant_2, "bright green")); 52 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.green_base_variant_3, "shy green")); 53 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.green_base_variant_4, "embarrassed green")); 54 | mPrimitiveAdapter.add(new Palette("Green Palette", 55 | R.color.green_base_variant_0, R.color.green_base_variant_2, R.color.green_base_variant_4)); 56 | 57 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.blue_base_variant_0, "dark blue")); 58 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.blue_base_variant_1, "blue")); 59 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.blue_base_variant_2, "bright blue")); 60 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.blue_base_variant_3, "shy blue")); 61 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.blue_base_variant_4, "embarrassed blue")); 62 | mPrimitiveAdapter.add(new Palette("Blue Palette", 63 | R.color.blue_base_variant_0, R.color.blue_base_variant_2, R.color.blue_base_variant_4)); 64 | 65 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.purple_base_variant_0, "dark purple")); 66 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.purple_base_variant_1, "purple")); 67 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.purple_base_variant_2, "bright purple")); 68 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.purple_base_variant_3, "shy purple")); 69 | mPrimitiveAdapter.add(new ColorNamePrimitive(R.color.purple_base_variant_4, "embarrassed purple")); 70 | mPrimitiveAdapter.add(new Palette("Purple Palette", 71 | R.color.purple_base_variant_0, R.color.purple_base_variant_2, R.color.purple_base_variant_4)); 72 | 73 | mPrimitiveAdapter.add(new Palette("Rainbow", 74 | R.color.red_base_variant_0, R.color.yellow_base_variant_0, R.color.green_base_variant_0, 75 | R.color.blue_base_variant_0, R.color.purple_base_variant_0)); 76 | 77 | mPrimitiveAdapter.add(new Palette("Strange Rainbow", 78 | R.color.red_base_variant_0, R.color.yellow_base_variant_1, R.color.green_base_variant_2, 79 | R.color.blue_base_variant_3, R.color.purple_base_variant_4)); 80 | 81 | recyclerView.setAdapter(mPrimitiveAdapter); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/PrimitiveAdapter.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | import com.tumblr.example.dagger.PerActivity; 6 | import com.tumblr.example.model.Primitive; 7 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 8 | import com.tumblr.graywater.GraywaterAdapter; 9 | 10 | import javax.inject.Inject; 11 | import javax.inject.Provider; 12 | import java.util.Map; 13 | 14 | /** 15 | * Example adapter. 16 | *

17 | * Created by ericleong on 3/13/16. 18 | */ 19 | @PerActivity 20 | public class PrimitiveAdapter extends GraywaterAdapter< 21 | Primitive, 22 | PrimitiveViewHolder, 23 | GraywaterAdapter.Binder, 24 | Class> { 25 | 26 | @NonNull 27 | private final Map, 28 | Provider>>> mItemBinderMap; 32 | 33 | @NonNull 34 | private final Map, 35 | Provider>> mActionListenerMap; 38 | 39 | @Inject 40 | public PrimitiveAdapter(final Map, ViewHolderCreator> viewHolderCreatorMapClass, 41 | @NonNull final Map, 42 | Provider>>> 46 | itemBinderMap, 47 | @NonNull final Map, 48 | Provider>> actionListenerMap) { 52 | 53 | for (Map.Entry, ViewHolderCreator> entry : viewHolderCreatorMapClass.entrySet()) { 54 | register(entry.getValue(), entry.getKey()); 55 | } 56 | 57 | mItemBinderMap = itemBinderMap; 58 | mActionListenerMap = actionListenerMap; 59 | } 60 | 61 | @Nullable 62 | @Override 63 | protected ItemBinder> 66 | getItemBinder(final Primitive model) { 67 | final Class modelType = getModelType(model); 68 | 69 | return mItemBinderMap.get(modelType).get(); 70 | } 71 | 72 | @Nullable 73 | @Override 74 | protected ActionListener 75 | getActionListener(final Primitive model) { 76 | final Class modelType = getModelType(model); 77 | final Provider> 78 | provider = mActionListenerMap.get(modelType); 79 | 80 | return provider != null ? provider.get() : null; 81 | } 82 | 83 | @NonNull 84 | @Override 85 | protected Class getModelType(final Primitive model) { 86 | return model.getClass(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/binder/ColorNameToastBinder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.binder; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | import com.tumblr.example.R; 6 | import com.tumblr.example.model.ColorNamePrimitive; 7 | import com.tumblr.example.viewholder.ColorPrimitiveViewHolder; 8 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 9 | import com.tumblr.graywater.GraywaterAdapter; 10 | 11 | import javax.inject.Inject; 12 | import javax.inject.Provider; 13 | import java.util.List; 14 | 15 | /** 16 | * Created by ericleong on 3/24/16. 17 | */ 18 | public class ColorNameToastBinder implements GraywaterAdapter.Binder { 19 | 20 | @Inject 21 | public ColorNameToastBinder() { 22 | 23 | } 24 | 25 | @Override 26 | public int getViewType(final ColorNamePrimitive model) { 27 | return R.layout.item_color; 28 | } 29 | 30 | @Override 31 | public void prepare(@NonNull final ColorNamePrimitive model, 32 | final List>> binderList, 34 | final int binderIndex) { 35 | 36 | } 37 | 38 | @Override 39 | public void bind(@NonNull final ColorNamePrimitive model, 40 | @NonNull final ColorPrimitiveViewHolder holder, 41 | @NonNull final List>> binderList, 43 | final int binderIndex, 44 | @Nullable final GraywaterAdapter.ActionListener< 45 | ColorNamePrimitive, PrimitiveViewHolder, ColorPrimitiveViewHolder> actionListener) { 46 | holder.getView().setBackgroundColor(holder.getView().getResources().getColor(model.getColor())); 47 | holder.getActionListenerDelegate().update(actionListener, model, holder, binderList, binderIndex, null); 48 | holder.getView().setOnClickListener(holder.getActionListenerDelegate()); 49 | } 50 | 51 | @Override 52 | public void unbind(@NonNull final ColorPrimitiveViewHolder holder) { 53 | holder.getView().setOnClickListener(null); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/binder/HeaderBinder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.binder; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | import com.tumblr.example.R; 6 | import com.tumblr.example.model.Primitive; 7 | import com.tumblr.example.viewholder.HeaderViewHolder; 8 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 9 | import com.tumblr.graywater.GraywaterAdapter; 10 | 11 | import javax.inject.Inject; 12 | import javax.inject.Provider; 13 | import java.util.List; 14 | 15 | /** 16 | * Created by ericleong on 3/13/16. 17 | */ 18 | public class HeaderBinder implements GraywaterAdapter.Binder { 19 | 20 | @Inject 21 | public HeaderBinder() { 22 | 23 | } 24 | 25 | @Override 26 | public int getViewType(final Primitive.Header model) { 27 | return R.layout.item_header; 28 | } 29 | 30 | @Override 31 | public void prepare(@NonNull final Primitive.Header model, 32 | final List>> binderList, 34 | final int binderIndex) { 35 | 36 | } 37 | 38 | @Override 39 | public void bind(@NonNull final Primitive.Header model, 40 | @NonNull final HeaderViewHolder holder, 41 | @NonNull final List>> binderList, 43 | final int binderIndex, 44 | @Nullable final GraywaterAdapter.ActionListener< 45 | Primitive.Header, PrimitiveViewHolder, HeaderViewHolder> actionListener) { 46 | 47 | } 48 | 49 | @Override 50 | public void unbind(@NonNull final HeaderViewHolder holder) { 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/binder/PaletteColorBinder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.binder; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | import android.view.View; 6 | import android.widget.Toast; 7 | import com.tumblr.example.R; 8 | import com.tumblr.example.model.Palette; 9 | import com.tumblr.example.viewholder.ColorPrimitiveViewHolder; 10 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 11 | import com.tumblr.graywater.GraywaterAdapter; 12 | 13 | import javax.inject.Inject; 14 | import javax.inject.Provider; 15 | import java.util.List; 16 | 17 | /** 18 | * Created by ericleong on 3/13/16. 19 | */ 20 | public class PaletteColorBinder implements GraywaterAdapter.Binder { 21 | 22 | @Inject 23 | public PaletteColorBinder() { 24 | 25 | } 26 | 27 | @Override 28 | public int getViewType(final Palette model) { 29 | return R.layout.item_color; 30 | } 31 | 32 | @Override 33 | public void prepare(@NonNull final Palette model, 34 | final List>> binderList, 36 | final int binderIndex) { 37 | 38 | } 39 | 40 | @Override 41 | public void bind(@NonNull final Palette model, 42 | @NonNull final ColorPrimitiveViewHolder holder, 43 | @NonNull final List>> binderList, 45 | final int binderIndex, 46 | @Nullable final GraywaterAdapter.ActionListener< 47 | Palette, PrimitiveViewHolder, ColorPrimitiveViewHolder> actionListener) { 48 | holder.getView().setBackgroundColor(holder.getView().getResources().getColor(model.getColors().get 49 | (binderIndex - 1))); 50 | 51 | holder.getView().setOnClickListener(new View.OnClickListener() { 52 | @Override 53 | public void onClick(final View v) { 54 | PaletteColorBinder.this.onClick(v, model, holder); 55 | } 56 | }); 57 | } 58 | 59 | @Override 60 | public void unbind(@NonNull final ColorPrimitiveViewHolder holder) { 61 | holder.getView().setOnClickListener(null); 62 | } 63 | 64 | public void onClick(final View v, final Palette model, final ColorPrimitiveViewHolder holder) { 65 | Toast.makeText(v.getContext(), model.getString(), Toast.LENGTH_SHORT).show(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/binder/TextPrimitiveBinder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.binder; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | import com.tumblr.example.R; 6 | import com.tumblr.example.model.Primitive; 7 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 8 | import com.tumblr.example.viewholder.TextPrimitiveViewHolder; 9 | import com.tumblr.graywater.GraywaterAdapter; 10 | 11 | import javax.inject.Inject; 12 | import javax.inject.Provider; 13 | import java.util.List; 14 | 15 | /** 16 | * Created by ericleong on 3/13/16. 17 | */ 18 | public class TextPrimitiveBinder 19 | implements GraywaterAdapter.Binder { 20 | 21 | @Inject 22 | public TextPrimitiveBinder() { 23 | 24 | } 25 | 26 | @Override 27 | public int getViewType(final T model) { 28 | return R.layout.item_text; 29 | } 30 | 31 | @Override 32 | public void prepare(@NonNull final T model, 33 | final List>> binderList, 35 | final int binderIndex) { 36 | 37 | } 38 | 39 | @Override 40 | public void bind(@NonNull final T model, 41 | @NonNull final TextPrimitiveViewHolder holder, 42 | @NonNull final List>> binderList, 44 | final int binderIndex, 45 | @Nullable final GraywaterAdapter.ActionListener actionListener) { 46 | holder.getTextView().setText(model.getString()); 47 | } 48 | 49 | @Override 50 | public void unbind(@NonNull final TextPrimitiveViewHolder holder) { 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/binderlist/ColorNamePrimitiveItemBinder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.binderlist; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | import android.view.View; 6 | import android.widget.Toast; 7 | import com.tumblr.example.PrimitiveAdapter; 8 | import com.tumblr.example.binder.ColorNameToastBinder; 9 | import com.tumblr.example.binder.TextPrimitiveBinder; 10 | import com.tumblr.example.dagger.PerActivity; 11 | import com.tumblr.example.model.ColorNamePrimitive; 12 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 13 | import com.tumblr.graywater.GraywaterAdapter; 14 | 15 | import javax.inject.Inject; 16 | import javax.inject.Provider; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | /** 21 | * Created by ericleong on 3/28/16. 22 | */ 23 | @PerActivity 24 | public class ColorNamePrimitiveItemBinder 25 | implements GraywaterAdapter.ItemBinder>, 27 | GraywaterAdapter.ActionListener { 28 | 29 | private final Provider> mColorNameTextBinder; 30 | private final Provider mColorNameToastBinder; 31 | 32 | private final Provider mAdapter; 33 | 34 | @Inject 35 | public ColorNamePrimitiveItemBinder(final Provider adapter, 36 | final Provider> colorNameTextBinder, 37 | final Provider colorNameToastBinder) { 38 | mColorNameTextBinder = colorNameTextBinder; 39 | mColorNameToastBinder = colorNameToastBinder; 40 | mAdapter = adapter; 41 | } 42 | 43 | @NonNull 44 | @Override 45 | public List>> 46 | getBinderList(@NonNull final ColorNamePrimitive model, final int position) { 47 | return new ArrayList>>() {{ 49 | add(mColorNameTextBinder); 50 | add(mColorNameToastBinder); 51 | }}; 52 | } 53 | 54 | @Override 55 | public void act(@NonNull final ColorNamePrimitive model, 56 | @NonNull final PrimitiveViewHolder holder, 57 | @NonNull final View v, 58 | @NonNull final List>> binderList, 60 | final int binderIndex, 61 | @Nullable final Object obj) { 62 | Toast.makeText(v.getContext(), model.getString(), Toast.LENGTH_SHORT).show(); 63 | 64 | final PrimitiveAdapter adapter = mAdapter.get(); 65 | 66 | adapter.add(adapter.getItemPosition(holder.getAdapterPosition()) + 1, 67 | new ColorNamePrimitive(model.getColor(), model.getString() + "+"), true); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/binderlist/HeaderPrimitiveItemBinder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.binderlist; 2 | 3 | import android.support.annotation.NonNull; 4 | import com.tumblr.example.binder.HeaderBinder; 5 | import com.tumblr.example.dagger.PerActivity; 6 | import com.tumblr.example.model.Primitive; 7 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 8 | import com.tumblr.graywater.GraywaterAdapter; 9 | 10 | import javax.inject.Inject; 11 | import javax.inject.Provider; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * Created by ericleong on 3/28/16. 17 | */ 18 | @PerActivity 19 | public class HeaderPrimitiveItemBinder implements 20 | GraywaterAdapter.ItemBinder> { 22 | private final Provider mHeaderBinder; 23 | 24 | @Inject 25 | public HeaderPrimitiveItemBinder(final Provider headerBinder) { 26 | mHeaderBinder = headerBinder; 27 | } 28 | 29 | @NonNull 30 | @Override 31 | public List>> 33 | getBinderList(@NonNull final Primitive.Header model, final int position) { 34 | return new ArrayList>>() {{ 36 | add(mHeaderBinder); 37 | }}; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/binderlist/PaletteItemBinder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.binderlist; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | import android.view.View; 6 | import com.tumblr.example.binder.PaletteColorBinder; 7 | import com.tumblr.example.binder.TextPrimitiveBinder; 8 | import com.tumblr.example.dagger.PerActivity; 9 | import com.tumblr.example.model.Palette; 10 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 11 | import com.tumblr.graywater.GraywaterAdapter; 12 | 13 | import javax.inject.Inject; 14 | import javax.inject.Provider; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | /** 19 | * Created by ericleong on 3/28/16. 20 | */ 21 | @PerActivity 22 | public class PaletteItemBinder implements GraywaterAdapter.ItemBinder>, 24 | GraywaterAdapter.ActionListener { 25 | private final Provider> mPaletteTextPrimitiveBinder; 26 | private final Provider mPaletteColorBinder; 27 | 28 | @Inject 29 | public PaletteItemBinder(final Provider> paletteTextPrimitiveBinder, 30 | final Provider paletteColorBinder) { 31 | mPaletteTextPrimitiveBinder = paletteTextPrimitiveBinder; 32 | mPaletteColorBinder = paletteColorBinder; 33 | } 34 | 35 | @NonNull 36 | @Override 37 | public List>> 38 | getBinderList(@NonNull final Palette model, final int position) { 39 | return new ArrayList>>() {{ 40 | add(mPaletteTextPrimitiveBinder); 41 | 42 | for (int color : model.getColors()) { 43 | add(mPaletteColorBinder); 44 | } 45 | }}; 46 | } 47 | 48 | @Override 49 | public void act(@NonNull final Palette model, 50 | @NonNull final PrimitiveViewHolder holder, 51 | @NonNull final View v, 52 | @NonNull final List>> binderList, 54 | final int binderIndex, 55 | @Nullable final Object obj) { 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/dagger/AppComponent.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.dagger; 2 | 3 | import com.tumblr.example.App; 4 | import com.tumblr.example.dagger.module.ActivityBindingModule; 5 | import dagger.Component; 6 | import dagger.android.AndroidInjector; 7 | import dagger.android.support.AndroidSupportInjectionModule; 8 | 9 | import javax.inject.Singleton; 10 | 11 | /** 12 | * Created by ericleong on 12/6/17. 13 | */ 14 | @Singleton 15 | @Component(modules = { 16 | AndroidSupportInjectionModule.class, 17 | ActivityBindingModule.class 18 | }) 19 | public interface AppComponent extends AndroidInjector { 20 | @Component.Builder 21 | abstract class Builder extends AndroidInjector.Builder {} 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/dagger/PerActivity.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.dagger; 2 | 3 | import javax.inject.Scope; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | 7 | /** 8 | * Activity scope. 9 | *

10 | * Created by ericleong on 12/6/17. 11 | */ 12 | @Scope 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface PerActivity { 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/dagger/key/PrimitiveCreatorKey.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.dagger.key; 2 | 3 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 4 | import dagger.MapKey; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | import static java.lang.annotation.ElementType.METHOD; 11 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 12 | 13 | /** 14 | * A {@link MapKey} annotation for maps with {@code Class} keys. 15 | * 16 | * Created by ericleong on 12/6/17. 17 | */ 18 | @Documented 19 | @Target(METHOD) 20 | @Retention(RUNTIME) 21 | @MapKey 22 | public @interface PrimitiveCreatorKey { 23 | Class value(); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/dagger/key/PrimitiveItemBinderKey.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.dagger.key; 2 | 3 | import com.tumblr.example.model.Primitive; 4 | import dagger.MapKey; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | import static java.lang.annotation.ElementType.METHOD; 11 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 12 | 13 | /** 14 | * A {@link MapKey} annotation for maps with {@code Class} keys. 15 | * 16 | * Created by ericleong on 12/6/17. 17 | */ 18 | @Documented 19 | @Target(METHOD) 20 | @Retention(RUNTIME) 21 | @MapKey 22 | public @interface PrimitiveItemBinderKey { 23 | Class value(); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/dagger/module/ActionListenerModule.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.dagger.module; 2 | 3 | import com.tumblr.example.binderlist.ColorNamePrimitiveItemBinder; 4 | import com.tumblr.example.dagger.PerActivity; 5 | import com.tumblr.example.dagger.key.PrimitiveItemBinderKey; 6 | import com.tumblr.example.model.ColorNamePrimitive; 7 | import com.tumblr.example.model.Primitive; 8 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 9 | import com.tumblr.graywater.GraywaterAdapter; 10 | import dagger.Binds; 11 | import dagger.Module; 12 | import dagger.multibindings.IntoMap; 13 | 14 | /** 15 | * Created by ericleong on 12/7/17. 16 | */ 17 | @Module 18 | public abstract class ActionListenerModule { 19 | @PerActivity 20 | @Binds 21 | @IntoMap 22 | @PrimitiveItemBinderKey(ColorNamePrimitive.class) 23 | abstract GraywaterAdapter.ActionListener 24 | bindsColorNamePrimitiveActionListener(ColorNamePrimitiveItemBinder colorNamePrimitiveItemBinder); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/dagger/module/ActivityBindingModule.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.dagger.module; 2 | 3 | import com.tumblr.example.MainActivity; 4 | import com.tumblr.example.dagger.PerActivity; 5 | import dagger.Module; 6 | import dagger.android.ContributesAndroidInjector; 7 | 8 | /** 9 | * Created by ericleong on 12/6/17. 10 | */ 11 | @Module 12 | public abstract class ActivityBindingModule { 13 | @PerActivity 14 | @ContributesAndroidInjector( 15 | modules = { 16 | ItemBinderModule.class, 17 | ViewHolderCreatorModule.class, 18 | ActionListenerModule.class 19 | } 20 | ) 21 | abstract MainActivity contributeMainActivityInjector(); 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/dagger/module/ItemBinderModule.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.dagger.module; 2 | 3 | import com.tumblr.example.binderlist.ColorNamePrimitiveItemBinder; 4 | import com.tumblr.example.binderlist.HeaderPrimitiveItemBinder; 5 | import com.tumblr.example.binderlist.PaletteItemBinder; 6 | import com.tumblr.example.dagger.PerActivity; 7 | import com.tumblr.example.dagger.key.PrimitiveItemBinderKey; 8 | import com.tumblr.example.model.ColorNamePrimitive; 9 | import com.tumblr.example.model.Palette; 10 | import com.tumblr.example.model.Primitive; 11 | import com.tumblr.example.viewholder.PrimitiveViewHolder; 12 | import com.tumblr.graywater.GraywaterAdapter; 13 | import dagger.Binds; 14 | import dagger.Module; 15 | import dagger.multibindings.IntoMap; 16 | 17 | /** 18 | * Created by ericleong on 12/6/17. 19 | */ 20 | @Module 21 | public abstract class ItemBinderModule { 22 | @PerActivity 23 | @Binds 24 | @IntoMap 25 | @PrimitiveItemBinderKey(ColorNamePrimitive.class) 26 | abstract GraywaterAdapter.ItemBinder< 27 | ? extends Primitive, 28 | ? extends PrimitiveViewHolder, 29 | ? extends GraywaterAdapter.Binder> 30 | bindsColorNamePrimitiveItemBinder(ColorNamePrimitiveItemBinder colorNamePrimitiveItemBinder); 31 | 32 | @PerActivity 33 | @Binds 34 | @IntoMap 35 | @PrimitiveItemBinderKey(Primitive.Header.class) 36 | abstract GraywaterAdapter.ItemBinder< 37 | ? extends Primitive, 38 | ? extends PrimitiveViewHolder, 39 | ? extends GraywaterAdapter.Binder> 40 | bindsHeaderPrimitiveItemBinder(HeaderPrimitiveItemBinder headerPrimitiveItemBinder); 41 | 42 | @PerActivity 43 | @Binds 44 | @IntoMap 45 | @PrimitiveItemBinderKey(Palette.class) 46 | abstract GraywaterAdapter.ItemBinder< 47 | ? extends Primitive, 48 | ? extends PrimitiveViewHolder, 49 | ? extends GraywaterAdapter.Binder> 50 | bindsPaletteItemBinder(PaletteItemBinder paletteItemBinder); 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/dagger/module/ViewHolderCreatorModule.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.dagger.module; 2 | 3 | import com.tumblr.example.dagger.key.PrimitiveCreatorKey; 4 | import com.tumblr.example.viewholder.ColorPrimitiveViewHolder; 5 | import com.tumblr.example.viewholder.HeaderViewHolder; 6 | import com.tumblr.example.viewholder.TextPrimitiveViewHolder; 7 | import com.tumblr.example.viewholdercreator.ColorPrimitiveViewHolderCreator; 8 | import com.tumblr.example.viewholdercreator.HeaderViewHolderCreator; 9 | import com.tumblr.example.viewholdercreator.TextPrimitiveViewHolderCreator; 10 | import com.tumblr.graywater.GraywaterAdapter; 11 | import dagger.Binds; 12 | import dagger.Module; 13 | import dagger.multibindings.IntoMap; 14 | 15 | /** 16 | * Created by ericleong on 12/6/17. 17 | */ 18 | @Module 19 | public abstract class ViewHolderCreatorModule { 20 | @Binds 21 | @IntoMap 22 | @PrimitiveCreatorKey(TextPrimitiveViewHolder.class) 23 | abstract GraywaterAdapter.ViewHolderCreator bindsTextPrimitiveViewHolderCreator( 24 | TextPrimitiveViewHolderCreator textPrimitiveViewHolderCreator); 25 | 26 | @Binds 27 | @IntoMap 28 | @PrimitiveCreatorKey(HeaderViewHolder.class) 29 | abstract GraywaterAdapter.ViewHolderCreator bindsHeaderViewHolderCreator(HeaderViewHolderCreator headerViewHolderCreator); 30 | 31 | @Binds 32 | @IntoMap 33 | @PrimitiveCreatorKey(ColorPrimitiveViewHolder.class) 34 | abstract GraywaterAdapter.ViewHolderCreator bindsColorPrimitiveViewHolderCreator( 35 | ColorPrimitiveViewHolderCreator colorPrimitiveViewHolderCreator); 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/model/ColorNamePrimitive.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.model; 2 | 3 | import android.support.annotation.ColorRes; 4 | 5 | /** 6 | * Created by ericleong on 3/13/16. 7 | */ 8 | public class ColorNamePrimitive implements Primitive.Color, Primitive.Text { 9 | @ColorRes 10 | private int color; 11 | 12 | private final String string; 13 | 14 | public ColorNamePrimitive(final int color, final String string) { 15 | this.color = color; 16 | this.string = string; 17 | } 18 | 19 | public void setColor(@ColorRes final int color) { 20 | this.color = color; 21 | } 22 | 23 | @ColorRes 24 | public int getColor() { 25 | return color; 26 | } 27 | 28 | public String getString() { 29 | return string; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/model/Palette.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.model; 2 | 3 | import android.support.annotation.ColorRes; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * A palette has 3 colors. 10 | *

11 | * Created by ericleong on 3/13/16. 12 | */ 13 | public class Palette implements Primitive, Primitive.Text { 14 | 15 | private String name; 16 | 17 | private List colors = new ArrayList<>(); 18 | 19 | public Palette(String name, @ColorRes int... colors) { 20 | this.name = name; 21 | 22 | for (int color : colors) { 23 | this.colors.add(color); 24 | } 25 | } 26 | 27 | @Override 28 | public String getString() { 29 | return name; 30 | } 31 | 32 | public List getColors() { 33 | return colors; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/model/Primitive.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.model; 2 | 3 | import android.support.annotation.ColorRes; 4 | 5 | /** 6 | * Created by ericleong on 3/13/16. 7 | */ 8 | public interface Primitive { 9 | 10 | interface Text extends Primitive { 11 | String getString(); 12 | } 13 | 14 | interface Color extends Primitive { 15 | @ColorRes 16 | int getColor(); 17 | } 18 | 19 | class Header implements Primitive { 20 | // Dummy marker class 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/model/TextPrimitive.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.model; 2 | 3 | /** 4 | * Created by ericleong on 3/13/16. 5 | */ 6 | public class TextPrimitive implements Primitive.Text { 7 | private final String string; 8 | 9 | public TextPrimitive(final String string) { 10 | this.string = string; 11 | } 12 | 13 | @Override 14 | public String getString() { 15 | return string; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/viewholder/ColorPrimitiveViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.viewholder; 2 | 3 | import android.view.View; 4 | import com.tumblr.example.R; 5 | import com.tumblr.example.model.ColorNamePrimitive; 6 | import com.tumblr.graywater.GraywaterAdapter; 7 | 8 | /** 9 | * Created by ericleong on 3/13/16. 10 | */ 11 | public class ColorPrimitiveViewHolder extends PrimitiveViewHolder { 12 | 13 | private GraywaterAdapter.ActionListenerDelegate 14 | mActionListenerDelegate = new GraywaterAdapter.ActionListenerDelegate<>(); 15 | 16 | private final View view; 17 | 18 | public ColorPrimitiveViewHolder(final View itemView) { 19 | super(itemView); 20 | view = itemView.findViewById(R.id.color); 21 | } 22 | 23 | public View getView() { 24 | return view; 25 | } 26 | 27 | public GraywaterAdapter.ActionListenerDelegate 28 | getActionListenerDelegate() { 29 | return mActionListenerDelegate; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/viewholder/HeaderViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.viewholder; 2 | 3 | import android.view.View; 4 | 5 | /** 6 | * Seems a bit ridiculous but helps us with type safety. 7 | */ 8 | public class HeaderViewHolder extends PrimitiveViewHolder { 9 | public HeaderViewHolder(final View itemView) { 10 | super(itemView); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/viewholder/PrimitiveViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.viewholder; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | 6 | /** 7 | * Created by ericleong on 3/13/16. 8 | */ 9 | public abstract class PrimitiveViewHolder extends RecyclerView.ViewHolder { 10 | public PrimitiveViewHolder(final View itemView) { 11 | super(itemView); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/viewholder/TextPrimitiveViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.viewholder; 2 | 3 | import android.view.View; 4 | import android.widget.TextView; 5 | import com.tumblr.example.R; 6 | 7 | /** 8 | * Created by ericleong on 3/13/16. 9 | */ 10 | public class TextPrimitiveViewHolder extends PrimitiveViewHolder { 11 | private final TextView textView; 12 | 13 | public TextPrimitiveViewHolder(final View itemView) { 14 | super(itemView); 15 | textView = (TextView) itemView.findViewById(R.id.text); 16 | } 17 | 18 | public TextView getTextView() { 19 | return textView; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/viewholdercreator/ColorPrimitiveViewHolderCreator.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.viewholdercreator; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.view.ViewGroup; 5 | import com.tumblr.example.R; 6 | import com.tumblr.example.viewholder.ColorPrimitiveViewHolder; 7 | import com.tumblr.graywater.GraywaterAdapter; 8 | 9 | import javax.inject.Inject; 10 | 11 | /** 12 | * Created by ericleong on 3/15/16. 13 | */ 14 | public class ColorPrimitiveViewHolderCreator implements GraywaterAdapter.ViewHolderCreator { 15 | 16 | @Inject 17 | public ColorPrimitiveViewHolderCreator() { 18 | 19 | } 20 | 21 | @Override 22 | public ColorPrimitiveViewHolder create(final ViewGroup parent) { 23 | return new ColorPrimitiveViewHolder(GraywaterAdapter.inflate(parent, R.layout.item_color)); 24 | } 25 | 26 | @Override 27 | public int getViewType() { 28 | return R.layout.item_color; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/viewholdercreator/HeaderViewHolderCreator.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.viewholdercreator; 2 | 3 | import android.view.ViewGroup; 4 | import com.tumblr.example.R; 5 | import com.tumblr.example.viewholder.HeaderViewHolder; 6 | import com.tumblr.graywater.GraywaterAdapter; 7 | 8 | import javax.inject.Inject; 9 | 10 | /** 11 | * Created by ericleong on 3/15/16. 12 | */ 13 | public class HeaderViewHolderCreator implements GraywaterAdapter.ViewHolderCreator { 14 | 15 | @Inject 16 | public HeaderViewHolderCreator() { 17 | 18 | } 19 | 20 | @Override 21 | public HeaderViewHolder create(final ViewGroup parent) { 22 | return new HeaderViewHolder(GraywaterAdapter.inflate(parent, R.layout.item_header)); 23 | } 24 | 25 | @Override 26 | public int getViewType() { 27 | return R.layout.item_header; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/tumblr/example/viewholdercreator/TextPrimitiveViewHolderCreator.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.example.viewholdercreator; 2 | 3 | import android.view.ViewGroup; 4 | import com.tumblr.example.R; 5 | import com.tumblr.example.viewholder.TextPrimitiveViewHolder; 6 | import com.tumblr.graywater.GraywaterAdapter; 7 | 8 | import javax.inject.Inject; 9 | 10 | /** 11 | * Created by ericleong on 3/15/16. 12 | */ 13 | public class TextPrimitiveViewHolderCreator implements GraywaterAdapter.ViewHolderCreator { 14 | 15 | @Inject 16 | public TextPrimitiveViewHolderCreator() { 17 | 18 | } 19 | 20 | @Override 21 | public TextPrimitiveViewHolder create(final ViewGroup parent) { 22 | return new TextPrimitiveViewHolder(GraywaterAdapter.inflate(parent, R.layout.item_text)); 23 | } 24 | 25 | @Override 26 | public int getViewType() { 27 | return R.layout.item_text; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_color.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_header.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_text.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tumblr/Graywater/662ba58886640c4af899e6a312d7723bed71369d/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tumblr/Graywater/662ba58886640c4af899e6a312d7723bed71369d/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tumblr/Graywater/662ba58886640c4af899e6a312d7723bed71369d/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tumblr/Graywater/662ba58886640c4af899e6a312d7723bed71369d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tumblr/Graywater/662ba58886640c4af899e6a312d7723bed71369d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | #d85f46 8 | #f94e27 9 | #fd4347 10 | #efc0b8 11 | #fe7576 12 | 13 | #f1993c 14 | #ffd731 15 | #fff148 16 | #decba6 17 | #e8ec75 18 | 19 | #5bbc8c 20 | #5bbe52 21 | #60cdb4 22 | #9dcaca 23 | #c1e4ab 24 | 25 | #56a0cb 26 | #8056a0cb 27 | #60cae0 28 | #558bd9 29 | #3b4fc8 30 | #6158c2 31 | 32 | #a780c1 33 | #b09dd4 34 | #d86dc3 35 | #e1b8d2 36 | #fc4081 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0dp 4 | 0dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | graywater 3 | This is a Header 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | google() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.0.1' 10 | 11 | // NOTE: Do not place your application dependencies here; they belong 12 | // in the individual module build.gradle files 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | jcenter() 19 | google() 20 | } 21 | } 22 | 23 | task clean(type: Delete) { 24 | delete rootProject.buildDir 25 | } 26 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tumblr/Graywater/662ba58886640c4af899e6a312d7723bed71369d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 05 14:40:30 EST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /graywater/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /graywater/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'checkstyle' 3 | 4 | android { 5 | compileSdkVersion 26 6 | buildToolsVersion '26.0.2' 7 | 8 | defaultConfig { 9 | minSdkVersion 16 10 | targetSdkVersion 26 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testImplementation'junit:junit:4.12' 25 | testImplementation "org.robolectric:robolectric:3.5" 26 | api 'com.android.support:recyclerview-v7:26.1.0' 27 | api 'javax.inject:javax.inject:1' 28 | } 29 | 30 | checkstyle { 31 | toolVersion = "6.7" 32 | } 33 | 34 | task checkstyle(type: Checkstyle) { 35 | configFile = rootProject.file('graywater/checkstyle.xml') 36 | configProperties = ['proj.module.dir': projectDir.absolutePath] 37 | classpath = files() 38 | 39 | source 'src/main/java' 40 | include '**/*.java' 41 | exclude '**/gen/**' 42 | } 43 | -------------------------------------------------------------------------------- /graywater/checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /graywater/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 36 | 38 | 39 | 40 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 52 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 139 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /graywater/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/ericleong/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /graywater/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /graywater/src/main/java/com/tumblr/graywater/GraywaterAdapter.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.graywater; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.support.annotation.AnyRes; 5 | import android.support.annotation.LayoutRes; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.Nullable; 8 | import android.support.annotation.VisibleForTesting; 9 | import android.support.v4.util.ArrayMap; 10 | import android.support.v4.util.Pair; 11 | import android.support.v7.widget.RecyclerView; 12 | import android.view.LayoutInflater; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | 16 | import javax.inject.Provider; 17 | import java.util.ArrayList; 18 | import java.util.HashSet; 19 | import java.util.List; 20 | import java.util.ListIterator; 21 | import java.util.Map; 22 | import java.util.Set; 23 | 24 | /** 25 | * Maps models to multiple view holders. 26 | *

27 | * Created by ericleong on 3/11/16. 28 | * 29 | * @param 30 | * the model type. 31 | * @param 32 | * the viewholder type. 33 | * @param 34 | * the binder type. 35 | * @param 36 | * the type of the model type ({@code Class} for example) 37 | */ 38 | public abstract class GraywaterAdapter< 39 | T, 40 | VH extends RecyclerView.ViewHolder, 41 | B extends GraywaterAdapter.Binder, 42 | MT> 43 | extends RecyclerView.Adapter { 44 | 45 | private static final int NO_PREVIOUS_BOUND_VIEWHOLDER = -1; 46 | 47 | /** 48 | * The T list for adapter items. 49 | */ 50 | @NonNull 51 | protected final List mItems = new ArrayList<>(); 52 | 53 | /** 54 | * Map of viewtypes to ViewHolderCreators. 55 | */ 56 | @NonNull 57 | private final Map mViewHolderCreatorMap = new ArrayMap<>(); 58 | 59 | /** 60 | * Map of viewtypes to the class of the viewholders they correspond to. This is just informational. 61 | */ 62 | @NonNull 63 | private final Map> mViewTypeToViewHolderClassMap = new ArrayMap<>(); 64 | 65 | /** 66 | * Map of viewtypes to the class of the viewholders they correspond to. This is just informational. 67 | */ 68 | @NonNull 69 | private final Map, Integer> mViewHolderClassToViewTypeMap = new ArrayMap<>(); 70 | 71 | /** 72 | * Map from model to a list of binders. A model may have multiple binders. 73 | */ 74 | @NonNull 75 | protected final Map> mItemBinderMap = new ArrayMap<>(); 76 | 77 | /** 78 | * Map from model to an action listener. A model may have multiple action listeners if there are multiple events. 79 | */ 80 | @NonNull 81 | private final Map> mActionListenerMap = new ArrayMap<>(); 82 | 83 | /** 84 | * Index of the last model that was bound. 85 | */ 86 | private int mPreviousBoundViewHolderPosition = NO_PREVIOUS_BOUND_VIEWHOLDER; 87 | 88 | private final List>>> mBinderListCache = new ArrayList<>(); 89 | private final List mViewHolderToItemPositionCache = new ArrayList<>(); 90 | private final List mItemPositionToFirstViewHolderPositionCache = new ArrayList<>(); 91 | /** 92 | * The viewholders that have had {@link #prepare(int, Binder, Object, List, int)} called on them. 93 | *

94 | * {@link #add(Object)}, {@link #remove(int)}, {@link #onViewRecycled(RecyclerView.ViewHolder)} 95 | * will all cause this to be cleared. 96 | */ 97 | private final Set mViewHolderPreparedCache = new HashSet<>(); 98 | 99 | /** 100 | * @param viewHolderCreator 101 | * the view holder creator to register. 102 | * @param viewHolderClass 103 | * the class of the viewholder (useful when debugging). 104 | */ 105 | protected void register(final ViewHolderCreator viewHolderCreator, final Class viewHolderClass) { 106 | final int viewType = viewHolderCreator.getViewType(); 107 | mViewHolderCreatorMap.put(viewType, viewHolderCreator); 108 | mViewTypeToViewHolderClassMap.put(viewType, viewHolderClass); 109 | mViewHolderClassToViewTypeMap.put(viewHolderClass, viewType); 110 | } 111 | 112 | /** 113 | * @param modelType 114 | * the model type. 115 | * @param parts 116 | * the binders to use to display this model. 117 | * @param listener 118 | * the listener to associate with the model. 119 | */ 120 | protected void register(@NonNull final MT modelType, 121 | @NonNull final ItemBinder parts, 122 | @Nullable final ActionListener listener) { 123 | mItemBinderMap.put(modelType, parts); 124 | mActionListenerMap.put(modelType, listener); 125 | } 126 | 127 | /** 128 | * @param model 129 | * the model to get the type of. 130 | * @return the appropriate type (for example, {@link Class}). 131 | */ 132 | @NonNull 133 | protected abstract MT getModelType(T model); 134 | 135 | /** 136 | * @param model 137 | * the model to get the {@link ItemBinder} for. 138 | * @return the {@link ItemBinder} for the given model. 139 | */ 140 | @Nullable 141 | protected ItemBinder getItemBinder(final T model) { 142 | return mItemBinderMap.get(getModelType(model)); 143 | } 144 | 145 | /** 146 | * @param model 147 | * the model to get the {@link ItemBinder} for. 148 | * @return the {@link ActionListener} for the given model. 149 | */ 150 | @Nullable 151 | protected ActionListener getActionListener(final T model) { 152 | return mActionListenerMap.get(getModelType(model)); 153 | } 154 | 155 | /** 156 | * @param model 157 | * the model to get parts for. 158 | * @param position 159 | * the position of the model. 160 | * @return the list of binders to use. 161 | */ 162 | @Nullable 163 | protected List>> getParts(final T model, final int position) { 164 | final List>> list; 165 | 166 | final ItemBinder itemBinder = getItemBinder(model); 167 | 168 | if (itemBinder != null) { 169 | list = itemBinder.getBinderList(model, position); 170 | } else { 171 | list = null; 172 | } 173 | 174 | return list; 175 | } 176 | 177 | /** 178 | * Computes the position of the item and the position of the binder within the item's binder list. 179 | * Note that this is an O(n) operation. 180 | * 181 | * @param viewHolderPosition 182 | * the position of the view holder in the adapter. 183 | * @return the item position and the position of the binder in the item's binder list. 184 | */ 185 | @VisibleForTesting 186 | BinderResult computeItemAndBinderIndex(final int viewHolderPosition) { 187 | // subtract off the length of each list until we get to the desired item 188 | 189 | final int itemIndex = mViewHolderToItemPositionCache.get(viewHolderPosition); 190 | final T item = mItems.get(itemIndex); 191 | final List>> binders = mBinderListCache.get(itemIndex); 192 | // index of the first item in the set of viewholders for the current item. 193 | final int firstVHPosForItem = mItemPositionToFirstViewHolderPositionCache.get(itemIndex); 194 | 195 | return new BinderResult(item, itemIndex, binders, viewHolderPosition - firstVHPosForItem); 196 | } 197 | 198 | @Override 199 | public int getItemViewType(final int position) { 200 | return internalGetItemViewType(position); 201 | } 202 | 203 | /** 204 | * Return the view type of the item at position for the purposes 205 | * of view recycling. 206 | * 207 | * @param viewHolderPosition 208 | * position to query 209 | * @return integer value identifying the type of the view needed to represent the item at 210 | * position. Type codes need not be contiguous. 211 | */ 212 | protected int internalGetItemViewType(final int viewHolderPosition) { 213 | final BinderResult result = computeItemAndBinderIndex(viewHolderPosition); 214 | 215 | final Provider> binder = result.getBinder(); 216 | final int viewType; 217 | 218 | if (binder != null) { 219 | viewType = getViewHolderCreatorMap().get(binder.get().getViewType(result.item)).getViewType(); 220 | } else { 221 | viewType = -1; 222 | } 223 | 224 | return viewType; 225 | } 226 | 227 | /** 228 | * @param viewType 229 | * the internal viewtype. 230 | * @return the viewholder class. 231 | */ 232 | protected Class getViewHolderClass(final int viewType) { 233 | return mViewTypeToViewHolderClassMap.get(viewType); 234 | } 235 | 236 | @NonNull 237 | protected Map getViewHolderCreatorMap() { 238 | return mViewHolderCreatorMap; 239 | } 240 | 241 | @Override 242 | public VH onCreateViewHolder(final ViewGroup parent, final int viewType) { 243 | return (VH) getViewHolderCreatorMap().get(viewType).create(parent); 244 | } 245 | 246 | @Override 247 | @SuppressLint("RecyclerView") 248 | public void onBindViewHolder(final VH holder, final int viewHolderPosition) { 249 | 250 | final BinderResult result = computeItemAndBinderIndex(viewHolderPosition); 251 | final Binder binder = result.getBinder().get(); 252 | 253 | if (binder != null && result.item != null) { 254 | 255 | if (mPreviousBoundViewHolderPosition == NO_PREVIOUS_BOUND_VIEWHOLDER) { 256 | prepare(viewHolderPosition, binder, result.item, result.binderList, result.binderIndex); 257 | } 258 | 259 | binder.bind(result.item, holder, result.binderList, result.binderIndex, getActionListener(result.item)); 260 | 261 | prepareInternal(viewHolderPosition); 262 | mPreviousBoundViewHolderPosition = viewHolderPosition; 263 | } 264 | } 265 | 266 | private void prepareInternal(final int viewHolderPosition) { 267 | prepare(viewHolderPosition, Integer.signum(viewHolderPosition - mPreviousBoundViewHolderPosition)); 268 | } 269 | 270 | /** 271 | * Calls {@link #prepare(int, Binder, Object, List, int)}. 272 | * 273 | * @param lastBoundViewHolderPosition 274 | * the position of the last viewholder that was bound. 275 | * @param direction 276 | * the direction the list is moving. 277 | */ 278 | protected void prepare(final int lastBoundViewHolderPosition, final int direction) { 279 | for (int i = 1; i <= numViewHoldersToPrepare(); i++) { 280 | final int viewHolderPosition = lastBoundViewHolderPosition + direction * i; 281 | if (isViewHolderPositionWithinBounds(viewHolderPosition)) { 282 | final BinderResult result = computeItemAndBinderIndex(viewHolderPosition); 283 | final Binder binder = result.getBinder().get(); 284 | 285 | if (binder != null && result.item != null) { 286 | prepare(viewHolderPosition, binder, result.item, result.binderList, result.binderIndex); 287 | } 288 | } 289 | } 290 | } 291 | 292 | /** 293 | * Calls {@link Binder#prepare(Object, List, int)}. 294 | * 295 | * @param viewHolderPosition 296 | * the position of the viewholder. 297 | * @param binder 298 | * the binder to call. 299 | * @param model 300 | * the model being prepared. 301 | * @param binderList 302 | * the list of binders 303 | * @param binderIndex 304 | * the index in the list of viewholders associated with this model 305 | */ 306 | protected void prepare(final int viewHolderPosition, 307 | final Binder binder, 308 | final T model, 309 | final List>> binderList, 310 | final int binderIndex) { 311 | if (!mViewHolderPreparedCache.contains(viewHolderPosition)) { 312 | binder.prepare(model, binderList, binderIndex); 313 | mViewHolderPreparedCache.add(viewHolderPosition); 314 | } 315 | } 316 | 317 | /** 318 | * @return Number of viewholders to prepare ahead. 319 | */ 320 | @SuppressWarnings("checkstyle:magicnumber") 321 | protected int numViewHoldersToPrepare() { 322 | return 3; 323 | } 324 | 325 | /** 326 | * Checks if the timeline position is within the bounds of the underlying List 327 | * 328 | * @param itemPosition 329 | * timeline item position. 330 | * @return true if within list bounds. False otherwise. 331 | */ 332 | protected boolean isItemPositionWithinBounds(final int itemPosition) { 333 | return itemPosition >= 0 && itemPosition < mItems.size(); 334 | } 335 | 336 | /** 337 | * Checks if the viewholder is within the bounds of underlying list. 338 | * 339 | * @param viewHolderPosition 340 | * viewholder position 341 | * @return true of within list bound. False otherwise. 342 | */ 343 | protected boolean isViewHolderPositionWithinBounds(final int viewHolderPosition) { 344 | return viewHolderPosition >= 0 && viewHolderPosition < mViewHolderToItemPositionCache.size(); 345 | } 346 | 347 | /** 348 | * Note that this is an O(n) operation, but it does not query for the list of binders. 349 | * 350 | * @param itemPosition 351 | * the position in the list of items. 352 | * @return the number of viewholders before the given item position. 353 | */ 354 | @VisibleForTesting 355 | public int getViewHolderCount(final int itemPosition) { 356 | if (itemPosition >= 0 && !mItemPositionToFirstViewHolderPositionCache.isEmpty()) { 357 | if (itemPosition >= mItemPositionToFirstViewHolderPositionCache.size()) { 358 | return mViewHolderToItemPositionCache.size(); 359 | } else { 360 | return mItemPositionToFirstViewHolderPositionCache.get(itemPosition); 361 | } 362 | } else { 363 | return 0; 364 | } 365 | } 366 | 367 | @Override 368 | public int getItemCount() { 369 | return mViewHolderToItemPositionCache.size(); 370 | } 371 | 372 | /** 373 | * Note that this does not notify. 374 | * 375 | * @param item 376 | * the item to add to the adapter. 377 | */ 378 | public void add(@NonNull final T item) { 379 | add(mItems.size(), item, true); 380 | } 381 | 382 | /** 383 | * @param item 384 | * the item to add to the adapter. 385 | * @param notify 386 | * whether or not to notify the adapter. 387 | */ 388 | public void add(@NonNull final T item, final boolean notify) { 389 | add(mItems.size(), item, notify); 390 | } 391 | 392 | /** 393 | * This is an O(1) operation since it is cached. 394 | * 395 | * @param viewHolderPosition 396 | * the position in the view holder. 397 | * @return the position of the item in the list of items. 398 | */ 399 | public int getItemPosition(final int viewHolderPosition) { 400 | if (isViewHolderPositionWithinBounds(viewHolderPosition)) { 401 | return mViewHolderToItemPositionCache.get(viewHolderPosition); 402 | } else { 403 | return -1; 404 | } 405 | } 406 | 407 | /** 408 | * @param itemIndex 409 | * the current view holders binder position. 410 | * @param viewHolderPosition 411 | * the view holder position. 412 | * @return the binder index associated with view holder. 413 | */ 414 | public int getBinderPosition(final int itemIndex, final int viewHolderPosition) { 415 | return viewHolderPosition - mItemPositionToFirstViewHolderPositionCache.get(itemIndex); 416 | } 417 | 418 | /** 419 | * Note that this is an O(n) operation, since the cache needs to be updated. 420 | * 421 | * @param position 422 | * the position to insert into the list. 423 | * @param item 424 | * the item to add. Note that if it is null, there is no way to determine which binder to use. 425 | * @param notify 426 | * whether or not to notify the adapter. 427 | */ 428 | public void add(final int position, @NonNull final T item, final boolean notify) { 429 | final int numViewHolders = getViewHolderCount(position); 430 | 431 | final List>> binders = getParts(item, position); 432 | 433 | mItems.add(position, item); 434 | mBinderListCache.add(position, binders); 435 | 436 | if (binders != null) { 437 | if (notify) { 438 | notifyItemRangeInserted(numViewHolders, binders.size()); 439 | } 440 | 441 | final List itemPositions = new ArrayList<>(); 442 | for (int i = 0; i < binders.size(); i++) { 443 | itemPositions.add(position); 444 | } 445 | 446 | mViewHolderToItemPositionCache.addAll(numViewHolders, itemPositions); 447 | for (int viewHolderIndex = numViewHolders + binders.size(); viewHolderIndex < mViewHolderToItemPositionCache.size(); 448 | viewHolderIndex++) { 449 | mViewHolderToItemPositionCache.set(viewHolderIndex, mViewHolderToItemPositionCache.get(viewHolderIndex) + 1); 450 | mViewHolderPreparedCache.remove(viewHolderIndex); 451 | } 452 | 453 | mItemPositionToFirstViewHolderPositionCache.add(position, numViewHolders); 454 | for (int itemIndex = position + 1; itemIndex < mItemPositionToFirstViewHolderPositionCache.size(); itemIndex++) { 455 | mItemPositionToFirstViewHolderPositionCache.set(itemIndex, 456 | mItemPositionToFirstViewHolderPositionCache.get(itemIndex) + binders.size()); 457 | } 458 | } 459 | } 460 | 461 | /** 462 | * Note that this is an O(n) operation, since the cache needs to be updated. 463 | * 464 | * @param itemPosition 465 | * removes the item at the position from the adapter. 466 | * @return the removed item, or null if the position was out of bounds. 467 | */ 468 | @Nullable 469 | public T remove(final int itemPosition) { 470 | return remove(itemPosition, true); 471 | } 472 | 473 | /** 474 | * Note that this is an O(n) operation, since the cache needs to be updated. 475 | * 476 | * @param itemPosition 477 | * removes the item at the position from the adapter. 478 | * @param notify 479 | * whether or not to call {@link #notifyItemRangeRemoved(int, int)} 480 | * @return the removed item, or null if the position was out of bounds. 481 | */ 482 | @Nullable 483 | public T remove(final int itemPosition, final boolean notify) { 484 | 485 | final T item; 486 | 487 | if (isItemPositionWithinBounds(itemPosition)) { 488 | final int numViewHolders = getViewHolderCount(itemPosition); 489 | 490 | item = mItems.get(itemPosition); 491 | 492 | final List>> binders = mBinderListCache.get(itemPosition); 493 | 494 | mItems.remove(itemPosition); 495 | 496 | for (final ListIterator iter = mViewHolderToItemPositionCache.listIterator(); iter.hasNext(); ) { 497 | if (iter.next() == itemPosition) { 498 | iter.remove(); 499 | } 500 | } 501 | 502 | for (int viewHolderIndex = numViewHolders; viewHolderIndex < mViewHolderToItemPositionCache.size(); 503 | viewHolderIndex++) { 504 | mViewHolderToItemPositionCache.set(viewHolderIndex, mViewHolderToItemPositionCache.get(viewHolderIndex) - 1); 505 | mViewHolderPreparedCache.remove(viewHolderIndex); 506 | } 507 | 508 | mItemPositionToFirstViewHolderPositionCache.remove(itemPosition); 509 | 510 | if (binders != null) { 511 | for (int itemIndex = itemPosition; itemIndex < mItemPositionToFirstViewHolderPositionCache.size(); itemIndex++) { 512 | mItemPositionToFirstViewHolderPositionCache.set(itemIndex, 513 | mItemPositionToFirstViewHolderPositionCache.get(itemIndex) - binders.size()); 514 | } 515 | } 516 | 517 | mBinderListCache.remove(itemPosition); 518 | 519 | if (binders != null && notify) { 520 | notifyItemRangeRemoved(numViewHolders, binders.size()); 521 | } 522 | } else { 523 | item = null; 524 | } 525 | 526 | return item; 527 | } 528 | 529 | /** 530 | * Finds the adapter data position for the first instance of a particular view holder used in an item. 531 | * 532 | * @param itemPosition 533 | * the position for the item that uses the view holder. 534 | * @param viewHolderClass 535 | * the view holder type to look for. 536 | * @return the adapter data position for the view holder, or -1 if not found. 537 | */ 538 | public int getFirstViewHolderPosition(final int itemPosition, @NonNull final Class viewHolderClass) { 539 | if (isItemPositionWithinBounds(itemPosition) && mViewHolderClassToViewTypeMap.containsKey(viewHolderClass)) { 540 | final int itemStartPos = getViewHolderCount(itemPosition); 541 | int viewHolderIndex = 0; 542 | final List>> binders = mBinderListCache.get(itemPosition); 543 | final int viewType = mViewHolderClassToViewTypeMap.get(viewHolderClass); 544 | final T item = mItems.get(itemPosition); 545 | for (Provider> binder : binders) { 546 | if (binder.get().getViewType(item) == viewType) { 547 | return itemStartPos + viewHolderIndex; 548 | } 549 | viewHolderIndex++; 550 | } 551 | } 552 | return -1; 553 | } 554 | 555 | /** 556 | * Clears the adapter. 557 | */ 558 | public void clear() { 559 | mItems.clear(); 560 | mBinderListCache.clear(); 561 | mViewHolderToItemPositionCache.clear(); 562 | mItemPositionToFirstViewHolderPositionCache.clear(); 563 | mViewHolderPreparedCache.clear(); 564 | mPreviousBoundViewHolderPosition = NO_PREVIOUS_BOUND_VIEWHOLDER; 565 | } 566 | 567 | /** 568 | * This is very similar to {@link View#inflate(android.content.Context, int, ViewGroup)} 569 | * but does not attach the inflated view. 570 | * 571 | * @param parent 572 | * the parent viewgroup 573 | * @param layoutRes 574 | * the layout to inflate 575 | * @return the inflated and unattached view. 576 | */ 577 | public static View inflate(final ViewGroup parent, @LayoutRes final int layoutRes) { 578 | return LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); 579 | } 580 | 581 | @Override 582 | public void onViewRecycled(final VH holder) { 583 | super.onViewRecycled(holder); 584 | 585 | onViewRecycled(holder, holder.getAdapterPosition()); 586 | } 587 | 588 | /** 589 | * @param holder 590 | * the viewholder to recycle. 591 | * @param viewHolderPosition 592 | * the position of the viewholder. 593 | */ 594 | protected void onViewRecycled(final VH holder, final int viewHolderPosition) { 595 | if (isViewHolderPositionWithinBounds(viewHolderPosition)) { 596 | final BinderResult result = computeItemAndBinderIndex(viewHolderPosition); 597 | final Binder binder = result.getBinder().get(); 598 | 599 | if (binder != null) { 600 | mViewHolderPreparedCache.remove(viewHolderPosition); 601 | 602 | if (holder.getItemViewType() == binder.getViewType(result.item)) { 603 | binder.unbind(holder); 604 | } 605 | } 606 | } 607 | } 608 | 609 | @NonNull 610 | public List getItems() { 611 | return mItems; 612 | } 613 | 614 | /** 615 | * @param itemPosition 616 | * the item position. 617 | * @return the binders that belong to the item at the given position. 618 | */ 619 | @Nullable 620 | public List>> getBindersForPosition(final int itemPosition) { 621 | final List>> binders; 622 | 623 | if (isItemPositionWithinBounds(itemPosition)) { 624 | binders = mBinderListCache.get(itemPosition); 625 | } else { 626 | binders = null; 627 | } 628 | 629 | return binders; 630 | } 631 | 632 | /** 633 | * @param itemPosition 634 | * the item position. 635 | * @return the range of viewholders that represent the item. The first is the offset, the second is the count. 636 | */ 637 | @Nullable 638 | public Pair getViewHolderRange(final int itemPosition) { 639 | final Pair range; 640 | 641 | if (isItemPositionWithinBounds(itemPosition)) { 642 | final int numViewHolders = getViewHolderCount(itemPosition); 643 | 644 | final List>> binders = mBinderListCache.get(itemPosition); 645 | 646 | range = new Pair<>(numViewHolders, binders.size()); 647 | } else { 648 | range = null; 649 | } 650 | 651 | return range; 652 | } 653 | 654 | /** 655 | * Binds a model of type {@code U} to a viewholder of type {@code V}. 656 | * 657 | * @param 658 | * the model. 659 | * @param 660 | * the viewholder type of the adapter. 661 | * @param 662 | * the viewholder type to be bound. 663 | */ 664 | public interface Binder { 665 | 666 | /** 667 | * @param model 668 | * the model that will be bound. 669 | * @return the type of the viewholder. 670 | */ 671 | @AnyRes 672 | int getViewType(U model); 673 | 674 | /** 675 | * Called to notify this binder that it may be called soon in the future. It may be called multiple times 676 | * before the view is actually ready to be bound. It is useful for preloading images. 677 | * 678 | * @param model 679 | * the model that will be bound. 680 | * @param binderList 681 | * the list of binders. 682 | * @param binderIndex 683 | * the index of the binder in the list of binders. 684 | */ 685 | void prepare(@NonNull U model, List>> binderList, int binderIndex); 686 | 687 | /** 688 | * Called when {@link android.support.v7.widget.RecyclerView.Adapter#onBindViewHolder(RecyclerView.ViewHolder, 689 | * int)} 690 | * is called. 691 | * 692 | * @param model 693 | * the model to bind to the viewholder 694 | * @param holder 695 | * the viewholder to update 696 | * @param binderList 697 | * the list of binders 698 | * @param binderIndex 699 | * the index in the list of viewholders associated with this model 700 | * @param actionListener 701 | * the action listener to use 702 | */ 703 | void bind(@NonNull U model, @NonNull W holder, @NonNull List>> binderList, 704 | int binderIndex, @Nullable ActionListener actionListener); 705 | 706 | /** 707 | * Called when {@link android.support.v7.widget.RecyclerView.Adapter#onViewRecycled(RecyclerView.ViewHolder)} 708 | * is called. 709 | * 710 | * @param holder 711 | * the view holder that was recycled. 712 | */ 713 | void unbind(@NonNull W holder); 714 | } 715 | 716 | /** 717 | * Creates a viewholder. 718 | */ 719 | public interface ViewHolderCreator { 720 | /** 721 | * Called when {@link android.support.v7.widget.RecyclerView.Adapter#onCreateViewHolder(ViewGroup, int)} 722 | * is called. 723 | * 724 | * @param parent 725 | * the parent view. 726 | * @return the inflated view. 727 | */ 728 | RecyclerView.ViewHolder create(ViewGroup parent); 729 | 730 | /** 731 | * In nearly all cases, this should simply return the layout id. 732 | * 733 | * @return the view type to associate with this {@link android.support.v7.widget.RecyclerView.ViewHolder}. 734 | */ 735 | int getViewType(); 736 | } 737 | 738 | /** 739 | * Gets the list of binders associated with an item. 740 | * 741 | * @param 742 | * the model type. 743 | * @param 744 | * the viewholder type. 745 | * @param 746 | * the binder type. 747 | */ 748 | public interface ItemBinder> { 749 | /** 750 | * @param model 751 | * the model that will be bound. 752 | * @param position 753 | * the position of the model in the list. 754 | * @return the list of binders to use. 755 | */ 756 | @NonNull 757 | List> getBinderList(@NonNull U model, int position); 758 | } 759 | 760 | /** 761 | * @param 762 | * the model type. 763 | * @param 764 | * the viewholder type from the adapter. 765 | * @param 766 | * the viewholder type. 767 | */ 768 | public interface ActionListener { 769 | /** 770 | * @param model 771 | * the model associated with the view that was modified. 772 | * @param holder 773 | * the viewholder associated with the view that was touched. 774 | * @param v 775 | * the view that was touched. 776 | * @param binderList 777 | * the list of binders associated with the model. 778 | * @param binderIndex 779 | * the index of the binder that was modified. 780 | * @param obj 781 | * an extra object for message passing. 782 | */ 783 | void act(@NonNull U model, @NonNull W holder, @NonNull View v, 784 | @NonNull List>> binderList, 785 | int binderIndex, @Nullable Object obj); 786 | } 787 | 788 | /** 789 | * A helper {@link android.view.View.OnClickListener} that can be used to hold references to objects that are 790 | * passed in during a {@link Binder#bind(Object, RecyclerView.ViewHolder, List, int, ActionListener)}. 791 | *

792 | * Note that it uses strong references. 793 | * 794 | * @param 795 | * the model type. 796 | * @param 797 | * the viewholder type of the adapter. 798 | * @param 799 | * the viewholder type to be bound. 800 | */ 801 | public static class ActionListenerDelegate 802 | implements View.OnClickListener { 803 | /** 804 | * The model. 805 | */ 806 | public U model; 807 | /** 808 | * The viewholder. 809 | */ 810 | public W holder; 811 | /** 812 | * The list of binders. 813 | */ 814 | public List>> binders; 815 | /** 816 | * The index into the list of binders. 817 | */ 818 | public int binderIndex; 819 | /** 820 | * A spare object to pass around. 821 | */ 822 | @Nullable 823 | public Object obj; 824 | /** 825 | * The listener to call on click. 826 | */ 827 | public ActionListener actionListener; 828 | 829 | /** 830 | * @param actionListener 831 | * the listener to call on click. 832 | * @param model 833 | * the model that is being clicked. 834 | * @param holder 835 | * the view holder that is being clicked. 836 | * @param binders 837 | * the list of binders associated with the model. 838 | * @param binderIndex 839 | * the index into the list of binders of the view holder that is being clicked. 840 | * @param obj 841 | * an extra object that can be used to pass around extra data. 842 | */ 843 | public void update(final ActionListener actionListener, 844 | @NonNull final U model, @NonNull final W holder, 845 | @NonNull final List>> binders, final int binderIndex, 846 | @Nullable final Object obj) { 847 | this.model = model; 848 | this.holder = holder; 849 | this.binders = binders; 850 | this.binderIndex = binderIndex; 851 | this.obj = obj; 852 | this.actionListener = actionListener; 853 | } 854 | 855 | @Override 856 | public void onClick(final View v) { 857 | actionListener.act(model, holder, v, binders, binderIndex, obj); 858 | } 859 | } 860 | 861 | /** 862 | * Internal class that holds the item and binder associated with a viewholder. 863 | */ 864 | @VisibleForTesting 865 | final class BinderResult { 866 | /** 867 | * The item associated with the viewholder. 868 | */ 869 | @Nullable 870 | public final T item; 871 | /** 872 | * The position of th item in the list of items. 873 | */ 874 | @VisibleForTesting 875 | public final int itemPosition; 876 | /** 877 | * The list of binders associated with the item. 878 | */ 879 | @Nullable 880 | public final List>> binderList; 881 | /** 882 | * The index of the binder to use in the list of binders. 883 | */ 884 | public final int binderIndex; 885 | 886 | /** 887 | * @param item 888 | * the model. 889 | * @param itemPosition 890 | * the position of the model in the list of models. 891 | * @param binderList 892 | * the list of binders associated with the item. 893 | * @param binderIndex 894 | * the index of the specific binder to use in the {@code binderList}. 895 | */ 896 | BinderResult(@Nullable final T item, 897 | final int itemPosition, 898 | @Nullable final List>> binderList, 899 | final int binderIndex) { 900 | this.item = item; 901 | this.itemPosition = itemPosition; 902 | this.binderList = binderList; 903 | this.binderIndex = binderIndex; 904 | } 905 | 906 | /** 907 | * @return the binder to use. 908 | */ 909 | @Nullable 910 | public Provider> getBinder() { 911 | return binderList != null && binderIndex >= 0 && binderIndex < binderList.size() 912 | ? binderList.get(binderIndex) : null; 913 | } 914 | } 915 | } 916 | -------------------------------------------------------------------------------- /graywater/src/test/java/com/tumblr/graywater/GraywaterAdapterTest.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.graywater; 2 | 3 | import android.net.Uri; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.robolectric.RobolectricTestRunner; 14 | 15 | import javax.inject.Provider; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | @RunWith(RobolectricTestRunner.class) 22 | public class GraywaterAdapterTest { 23 | 24 | private abstract static class TestBinder 25 | implements GraywaterAdapter.Binder { 26 | 27 | } 28 | 29 | private static class TestAdapter 30 | extends GraywaterAdapter< 31 | Object, 32 | RecyclerView.ViewHolder, 33 | TestBinder, 34 | Class> { 35 | 36 | @Override 37 | protected Class getModelType(final Object model) { 38 | 39 | Class modelType = model.getClass(); 40 | 41 | // Types are messy. 42 | final ItemBinder itemBinder = mItemBinderMap.get(modelType); 43 | 44 | final Class declaringClass = modelType.getDeclaringClass(); 45 | if (itemBinder == null && declaringClass != null) { 46 | modelType = declaringClass; 47 | } 48 | 49 | return modelType; 50 | } 51 | 52 | private static class TextViewHolder extends RecyclerView.ViewHolder { 53 | TextViewHolder(final View itemView) { 54 | super(itemView); 55 | } 56 | } 57 | 58 | private static class ImageViewHolder extends RecyclerView.ViewHolder { 59 | ImageViewHolder(final View itemView) { 60 | super(itemView); 61 | } 62 | } 63 | 64 | private static class TextViewHolderCreator implements GraywaterAdapter.ViewHolderCreator { 65 | 66 | static final int VIEW_TYPE = 1; 67 | 68 | @Override 69 | public TextViewHolder create(final ViewGroup parent) { 70 | return new TextViewHolder(new TextView(parent.getContext())); 71 | } 72 | 73 | @Override 74 | public int getViewType() { 75 | return VIEW_TYPE; 76 | } 77 | } 78 | 79 | private static class ImageViewHolderCreator implements GraywaterAdapter.ViewHolderCreator { 80 | 81 | static final int VIEW_TYPE = 2; 82 | 83 | @Override 84 | public ImageViewHolder create(final ViewGroup parent) { 85 | return new ImageViewHolder(new ImageView(parent.getContext())); 86 | } 87 | 88 | @Override 89 | public int getViewType() { 90 | return VIEW_TYPE; 91 | } 92 | } 93 | 94 | private static class TextBinder extends TestBinder { 95 | 96 | @Override 97 | public int getViewType(@NonNull final String model) { 98 | return TextViewHolderCreator.VIEW_TYPE; 99 | } 100 | 101 | @Override 102 | public void prepare(@NonNull final String model, 103 | final List>> binderList, 105 | final int binderIndex) { 106 | 107 | } 108 | 109 | @Override 110 | public void bind(@NonNull final String model, 111 | @NonNull final TextViewHolder holder, 112 | @NonNull final List>> binderList, 114 | final int binderIndex, 115 | @Nullable final ActionListener actionListener) { 116 | ((TextView) holder.itemView).setText(model); 117 | } 118 | 119 | @Override 120 | public void unbind(@NonNull final TextViewHolder holder) { 121 | ((TextView) holder.itemView).setText(""); 122 | } 123 | } 124 | 125 | private static class ImageBinder extends TestBinder { 126 | @Override 127 | public int getViewType(@NonNull final Uri model) { 128 | return ImageViewHolderCreator.VIEW_TYPE; 129 | } 130 | 131 | @Override 132 | public void prepare(@NonNull final Uri model, 133 | final List>> binderList, 135 | final int binderIndex) { 136 | 137 | } 138 | 139 | @Override 140 | public void bind(@NonNull final Uri model, 141 | @NonNull final ImageViewHolder holder, 142 | @NonNull final List>> binderList, 144 | final int binderIndex, 145 | @Nullable final ActionListener actionListener) { 146 | ((ImageView) holder.itemView).setImageURI(model); // not a good idea in production ;) 147 | } 148 | 149 | @Override 150 | public void unbind(@NonNull final ImageViewHolder holder) { 151 | 152 | } 153 | } 154 | 155 | public TestAdapter() { 156 | super(); 157 | 158 | register(new TextViewHolderCreator(), TextViewHolder.class); 159 | register(new ImageViewHolderCreator(), ImageViewHolder.class); 160 | 161 | final Provider textBinder = new Provider() { 162 | @Override 163 | public TextBinder get() { 164 | return new TextBinder(); 165 | } 166 | }; 167 | final Provider imageBinder = new Provider() { 168 | @Override 169 | public ImageBinder get() { 170 | return new ImageBinder(); 171 | } 172 | }; 173 | 174 | register( 175 | String.class, 176 | new ItemBinder>() { 178 | @NonNull 179 | @Override 180 | public List>> 181 | getBinderList(@NonNull final String model, final int position) { 182 | return new ArrayList>>() {{ 184 | add(textBinder); 185 | add(textBinder); 186 | }}; 187 | } 188 | }, null); 189 | 190 | register( 191 | Uri.class, 192 | new ItemBinder>() { 194 | @NonNull 195 | @Override 196 | public List>> getBinderList(@NonNull final Uri model, final int position) { 198 | return new ArrayList< 199 | Provider>>() {{ 200 | add(imageBinder); 201 | add(imageBinder); 202 | add(imageBinder); 203 | }}; 204 | } 205 | }, null); 206 | } 207 | } 208 | 209 | @Test 210 | public void testAdd() throws Exception { 211 | final TestAdapter adapter = new TestAdapter(); 212 | 213 | adapter.add("one"); 214 | assertEquals(2, adapter.getItemCount()); 215 | 216 | adapter.add("two"); 217 | assertEquals(4, adapter.getItemCount()); 218 | 219 | adapter.add("three"); 220 | assertEquals(6, adapter.getItemCount()); 221 | } 222 | 223 | @Test 224 | public void testRemove() throws Exception { 225 | final TestAdapter adapter = new TestAdapter(); 226 | 227 | adapter.add("zero"); 228 | adapter.add("one"); 229 | adapter.add("two"); 230 | adapter.add("three"); 231 | adapter.add("four"); 232 | adapter.add("five"); 233 | 234 | // remove 235 | adapter.remove(0); 236 | assertEquals(2 * 5, adapter.getItemCount()); 237 | adapter.remove(4); 238 | assertEquals(2 * 4, adapter.getItemCount()); 239 | adapter.remove(2); 240 | assertEquals(2 * 3, adapter.getItemCount()); 241 | 242 | // state 243 | final List items = adapter.getItems(); 244 | assertEquals(3, items.size()); 245 | assertEquals("one", items.get(0)); 246 | assertEquals("two", items.get(1)); 247 | assertEquals("four", items.get(2)); 248 | } 249 | 250 | @Test 251 | public void testViewHolderPosition() throws Exception { 252 | final TestAdapter adapter = new TestAdapter(); 253 | 254 | adapter.add("zero"); 255 | adapter.add("one"); 256 | adapter.add("two"); 257 | adapter.add("three"); 258 | adapter.add("four"); 259 | adapter.add("five"); 260 | 261 | // ["zero", "zero", "one", "one, ... ] 262 | final GraywaterAdapter.BinderResult one = adapter.computeItemAndBinderIndex(2); 263 | assertEquals(1, one.itemPosition); 264 | assertEquals(0, one.binderIndex); 265 | assertEquals("one", one.item); 266 | 267 | final GraywaterAdapter.BinderResult oneDouble = adapter.computeItemAndBinderIndex(3); 268 | assertEquals(1, oneDouble.itemPosition); 269 | assertEquals(1, oneDouble.binderIndex); 270 | assertEquals("one", oneDouble.item); 271 | 272 | final GraywaterAdapter.BinderResult three = adapter.computeItemAndBinderIndex(6); 273 | assertEquals(3, three.itemPosition); 274 | assertEquals(0, three.binderIndex); 275 | assertEquals("three", three.item); 276 | 277 | final GraywaterAdapter.BinderResult threeDouble = adapter.computeItemAndBinderIndex(7); 278 | assertEquals(3, threeDouble.itemPosition); 279 | assertEquals(1, threeDouble.binderIndex); 280 | assertEquals("three", three.item); 281 | 282 | final GraywaterAdapter.BinderResult zero = adapter.computeItemAndBinderIndex(0); 283 | assertEquals(0, zero.itemPosition); 284 | assertEquals(0, zero.binderIndex); 285 | assertEquals("zero", zero.item); 286 | 287 | final GraywaterAdapter.BinderResult five = adapter.computeItemAndBinderIndex(11); 288 | assertEquals(5, five.itemPosition); 289 | assertEquals(1, five.binderIndex); 290 | assertEquals("five", five.item); 291 | } 292 | 293 | @Test 294 | public void testViewHolderPositionWithRemove() throws Exception { 295 | final TestAdapter adapter = new TestAdapter(); 296 | 297 | adapter.add("zero"); 298 | adapter.add("one"); 299 | adapter.add("two"); 300 | adapter.add("three"); 301 | adapter.add("four"); 302 | adapter.add("five"); 303 | 304 | adapter.remove(0); 305 | 306 | // ["one", "one, "two", "two", ... ] 307 | final GraywaterAdapter.BinderResult two = adapter.computeItemAndBinderIndex(2); 308 | assertEquals(1, two.itemPosition); 309 | assertEquals(0, two.binderIndex); 310 | 311 | // four is in position five 312 | final GraywaterAdapter.BinderResult four = adapter.computeItemAndBinderIndex(9); 313 | assertEquals(4, four.itemPosition); 314 | assertEquals(1, four.binderIndex); 315 | } 316 | 317 | @Test 318 | public void testViewHolderPositionWithRemoveThenAdd() throws Exception { 319 | final TestAdapter adapter = new TestAdapter(); 320 | 321 | adapter.add("zero"); 322 | adapter.add("one"); 323 | adapter.add("two"); 324 | adapter.add("three"); 325 | adapter.add("four"); 326 | adapter.add("five"); 327 | 328 | adapter.remove(0); 329 | 330 | adapter.add(0, "zero", false); 331 | 332 | // ["zero", "zero", "one", "one, ... ] 333 | final GraywaterAdapter.BinderResult one = adapter.computeItemAndBinderIndex(2); 334 | assertEquals(1, one.itemPosition); 335 | assertEquals(0, one.binderIndex); 336 | assertEquals("one", one.item); 337 | 338 | final GraywaterAdapter.BinderResult oneDouble = adapter.computeItemAndBinderIndex(3); 339 | assertEquals(1, oneDouble.itemPosition); 340 | assertEquals(1, oneDouble.binderIndex); 341 | assertEquals("one", oneDouble.item); 342 | 343 | final GraywaterAdapter.BinderResult three = adapter.computeItemAndBinderIndex(6); 344 | assertEquals(3, three.itemPosition); 345 | assertEquals(0, three.binderIndex); 346 | assertEquals("three", three.item); 347 | 348 | final GraywaterAdapter.BinderResult threeDouble = adapter.computeItemAndBinderIndex(7); 349 | assertEquals(3, threeDouble.itemPosition); 350 | assertEquals(1, threeDouble.binderIndex); 351 | assertEquals("three", three.item); 352 | 353 | final GraywaterAdapter.BinderResult zero = adapter.computeItemAndBinderIndex(0); 354 | assertEquals(0, zero.itemPosition); 355 | assertEquals(0, zero.binderIndex); 356 | assertEquals("zero", zero.item); 357 | 358 | final GraywaterAdapter.BinderResult five = adapter.computeItemAndBinderIndex(11); 359 | assertEquals(5, five.itemPosition); 360 | assertEquals(1, five.binderIndex); 361 | assertEquals("five", five.item); 362 | } 363 | 364 | @Test 365 | public void testViewHolderPositionWithRemoveThenAddMiddle() throws Exception { 366 | final TestAdapter adapter = new TestAdapter(); 367 | 368 | adapter.add("zero"); 369 | adapter.add("one"); 370 | adapter.add("two"); 371 | adapter.add("three"); 372 | adapter.add("four"); 373 | adapter.add("five"); 374 | 375 | final Object obj = adapter.remove(2); 376 | assertEquals("two", obj); 377 | 378 | adapter.add(2, "two", false); 379 | 380 | // ["zero", "zero", "one", "one, ... ] 381 | final GraywaterAdapter.BinderResult one = adapter.computeItemAndBinderIndex(2); 382 | assertEquals(1, one.itemPosition); 383 | assertEquals(0, one.binderIndex); 384 | assertEquals("one", one.item); 385 | 386 | final GraywaterAdapter.BinderResult oneDouble = adapter.computeItemAndBinderIndex(3); 387 | assertEquals(1, oneDouble.itemPosition); 388 | assertEquals(1, oneDouble.binderIndex); 389 | assertEquals("one", oneDouble.item); 390 | 391 | final GraywaterAdapter.BinderResult three = adapter.computeItemAndBinderIndex(6); 392 | assertEquals(3, three.itemPosition); 393 | assertEquals(0, three.binderIndex); 394 | assertEquals("three", three.item); 395 | 396 | final GraywaterAdapter.BinderResult threeDouble = adapter.computeItemAndBinderIndex(7); 397 | assertEquals(3, threeDouble.itemPosition); 398 | assertEquals(1, threeDouble.binderIndex); 399 | assertEquals("three", three.item); 400 | 401 | final GraywaterAdapter.BinderResult zero = adapter.computeItemAndBinderIndex(0); 402 | assertEquals(0, zero.itemPosition); 403 | assertEquals(0, zero.binderIndex); 404 | assertEquals("zero", zero.item); 405 | 406 | final GraywaterAdapter.BinderResult five = adapter.computeItemAndBinderIndex(11); 407 | assertEquals(5, five.itemPosition); 408 | assertEquals(1, five.binderIndex); 409 | assertEquals("five", five.item); 410 | } 411 | 412 | @Test 413 | public void testGetItemViewType() throws Exception { 414 | final TestAdapter adapter = new TestAdapter(); 415 | 416 | // ["https://www.tumblr.com", "https://www.tumblr.com", "https://www.tumblr.com", "one", "one", 417 | // "http://dreamynomad.com", "http://dreamynomad.com", "http://dreamynomad.com", ...] 418 | adapter.add(Uri.parse("https://www.tumblr.com")); 419 | assertEquals(3, adapter.getItemCount()); 420 | adapter.add("one"); 421 | assertEquals(5, adapter.getItemCount()); 422 | adapter.add(Uri.parse("http://dreamynomad.com")); 423 | assertEquals(8, adapter.getItemCount()); 424 | adapter.add("three"); 425 | assertEquals(10, adapter.getItemCount()); 426 | adapter.add(Uri.parse("https://google.com")); 427 | assertEquals(13, adapter.getItemCount()); 428 | adapter.add("five"); 429 | assertEquals(15, adapter.getItemCount()); 430 | 431 | assertEquals(TestAdapter.ImageViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(0)); 432 | assertEquals(TestAdapter.ImageViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(1)); 433 | assertEquals(TestAdapter.ImageViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(2)); 434 | assertEquals(TestAdapter.TextViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(3)); 435 | assertEquals(TestAdapter.TextViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(4)); 436 | assertEquals(TestAdapter.ImageViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(5)); 437 | assertEquals(TestAdapter.ImageViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(6)); 438 | // ... 439 | assertEquals(TestAdapter.ImageViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(10)); 440 | assertEquals(TestAdapter.ImageViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(11)); 441 | assertEquals(TestAdapter.ImageViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(12)); 442 | assertEquals(TestAdapter.TextViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(13)); 443 | assertEquals(TestAdapter.TextViewHolderCreator.VIEW_TYPE, adapter.getItemViewType(14)); 444 | } 445 | 446 | @Test 447 | public void testMultiViewHolderPosition() throws Exception { 448 | final TestAdapter adapter = new TestAdapter(); 449 | 450 | // ["https://www.tumblr.com", "https://www.tumblr.com", "https://www.tumblr.com", "one", "one", 451 | // "http://dreamynomad.com", "http://dreamynomad.com", "http://dreamynomad.com", ...] 452 | final Uri tumblrUri = Uri.parse("https://www.tumblr.com"); 453 | adapter.add(tumblrUri); 454 | assertEquals(3, adapter.getItemCount()); 455 | adapter.add("one"); 456 | assertEquals(5, adapter.getItemCount()); 457 | adapter.add(Uri.parse("http://dreamynomad.com")); 458 | assertEquals(8, adapter.getItemCount()); 459 | adapter.add("three"); 460 | assertEquals(10, adapter.getItemCount()); 461 | final Uri googleUri = Uri.parse("https://google.com"); 462 | adapter.add(googleUri); 463 | assertEquals(13, adapter.getItemCount()); 464 | adapter.add("five"); 465 | assertEquals(15, adapter.getItemCount()); 466 | 467 | final GraywaterAdapter.BinderResult tumblr = adapter.computeItemAndBinderIndex(1); 468 | assertEquals(0, tumblr.itemPosition); 469 | assertEquals(1, tumblr.binderIndex); 470 | assertEquals(tumblrUri, tumblr.item); 471 | 472 | final GraywaterAdapter.BinderResult one = adapter.computeItemAndBinderIndex(3); 473 | assertEquals(1, one.itemPosition); 474 | assertEquals(0, one.binderIndex); 475 | assertEquals("one", one.item); 476 | 477 | final GraywaterAdapter.BinderResult google = adapter.computeItemAndBinderIndex(12); 478 | assertEquals(4, google.itemPosition); 479 | assertEquals(2, google.binderIndex); 480 | assertEquals(googleUri, google.item); 481 | 482 | final GraywaterAdapter.BinderResult five = adapter.computeItemAndBinderIndex(14); 483 | assertEquals(5, five.itemPosition); 484 | assertEquals(1, five.binderIndex); 485 | assertEquals("five", five.item); 486 | } 487 | 488 | @Test 489 | public void testMultiViewHolderPositionWithRemoveThenAdd() throws Exception { 490 | final TestAdapter adapter = new TestAdapter(); 491 | 492 | // ["https://www.tumblr.com", "https://www.tumblr.com", "https://www.tumblr.com", "one", "one", 493 | // "http://dreamynomad.com", "http://dreamynomad.com", "http://dreamynomad.com", ...] 494 | final Uri tumblrUri = Uri.parse("https://www.tumblr.com"); 495 | adapter.add(tumblrUri); 496 | assertEquals(3, adapter.getItemCount()); 497 | adapter.add("one"); 498 | assertEquals(5, adapter.getItemCount()); 499 | adapter.add(Uri.parse("http://dreamynomad.com")); 500 | assertEquals(8, adapter.getItemCount()); 501 | adapter.add("three"); 502 | assertEquals(10, adapter.getItemCount()); 503 | final Uri googleUri = Uri.parse("https://google.com"); 504 | adapter.add(googleUri); 505 | assertEquals(13, adapter.getItemCount()); 506 | adapter.add("five"); 507 | assertEquals(15, adapter.getItemCount()); 508 | 509 | adapter.remove(3); 510 | adapter.add(3, "three", false); 511 | 512 | final GraywaterAdapter.BinderResult tumblr = adapter.computeItemAndBinderIndex(1); 513 | assertEquals(0, tumblr.itemPosition); 514 | assertEquals(1, tumblr.binderIndex); 515 | assertEquals(tumblrUri, tumblr.item); 516 | 517 | final GraywaterAdapter.BinderResult one = adapter.computeItemAndBinderIndex(3); 518 | assertEquals(1, one.itemPosition); 519 | assertEquals(0, one.binderIndex); 520 | assertEquals("one", one.item); 521 | 522 | final GraywaterAdapter.BinderResult google = adapter.computeItemAndBinderIndex(12); 523 | assertEquals(4, google.itemPosition); 524 | assertEquals(2, google.binderIndex); 525 | assertEquals(googleUri, google.item); 526 | 527 | final GraywaterAdapter.BinderResult five = adapter.computeItemAndBinderIndex(14); 528 | assertEquals(5, five.itemPosition); 529 | assertEquals(1, five.binderIndex); 530 | assertEquals("five", five.item); 531 | } 532 | 533 | @Test 534 | public void testClear() throws Exception { 535 | final TestAdapter adapter = new TestAdapter(); 536 | 537 | // ["https://www.tumblr.com", "https://www.tumblr.com", "https://www.tumblr.com", "one", "one", 538 | // "http://dreamynomad.com", "http://dreamynomad.com", "http://dreamynomad.com", ...] 539 | final Uri tumblrUri = Uri.parse("https://www.tumblr.com"); 540 | adapter.add(tumblrUri); 541 | assertEquals(3, adapter.getItemCount()); 542 | adapter.add("one"); 543 | assertEquals(5, adapter.getItemCount()); 544 | adapter.add(Uri.parse("http://dreamynomad.com")); 545 | assertEquals(8, adapter.getItemCount()); 546 | adapter.add("three"); 547 | assertEquals(10, adapter.getItemCount()); 548 | final Uri googleUri = Uri.parse("https://google.com"); 549 | adapter.add(googleUri); 550 | assertEquals(13, adapter.getItemCount()); 551 | adapter.add("five"); 552 | assertEquals(15, adapter.getItemCount()); 553 | 554 | adapter.clear(); 555 | 556 | assertEquals(0, adapter.getItemCount()); 557 | } 558 | 559 | @Test 560 | public void testClearThenAdd() throws Exception { 561 | final TestAdapter adapter = new TestAdapter(); 562 | 563 | // ["https://www.tumblr.com", "https://www.tumblr.com", "https://www.tumblr.com", "one", "one", 564 | // "http://dreamynomad.com", "http://dreamynomad.com", "http://dreamynomad.com", ...] 565 | final Uri tumblrUri = Uri.parse("https://www.tumblr.com"); 566 | adapter.add(tumblrUri); 567 | assertEquals(3, adapter.getItemCount()); 568 | adapter.add("one"); 569 | assertEquals(5, adapter.getItemCount()); 570 | adapter.add(Uri.parse("http://dreamynomad.com")); 571 | assertEquals(8, adapter.getItemCount()); 572 | adapter.add("three"); 573 | assertEquals(10, adapter.getItemCount()); 574 | final Uri googleUri = Uri.parse("https://google.com"); 575 | adapter.add(googleUri); 576 | assertEquals(13, adapter.getItemCount()); 577 | adapter.add("five"); 578 | assertEquals(15, adapter.getItemCount()); 579 | 580 | // Clear! 581 | adapter.clear(); 582 | 583 | // ["https://www.tumblr.com", "https://www.tumblr.com", "https://www.tumblr.com", "one", "one", 584 | // "http://dreamynomad.com", "http://dreamynomad.com", "http://dreamynomad.com", ...] 585 | adapter.add(tumblrUri); 586 | assertEquals(3, adapter.getItemCount()); 587 | adapter.add("one"); 588 | assertEquals(5, adapter.getItemCount()); 589 | adapter.add(Uri.parse("http://dreamynomad.com")); 590 | assertEquals(8, adapter.getItemCount()); 591 | adapter.add("three"); 592 | assertEquals(10, adapter.getItemCount()); 593 | adapter.add(googleUri); 594 | assertEquals(13, adapter.getItemCount()); 595 | adapter.add("five"); 596 | assertEquals(15, adapter.getItemCount()); 597 | 598 | final GraywaterAdapter.BinderResult tumblr = adapter.computeItemAndBinderIndex(1); 599 | assertEquals(0, tumblr.itemPosition); 600 | assertEquals(1, tumblr.binderIndex); 601 | assertEquals(tumblrUri, tumblr.item); 602 | 603 | final GraywaterAdapter.BinderResult one = adapter.computeItemAndBinderIndex(3); 604 | assertEquals(1, one.itemPosition); 605 | assertEquals(0, one.binderIndex); 606 | assertEquals("one", one.item); 607 | 608 | final GraywaterAdapter.BinderResult google = adapter.computeItemAndBinderIndex(12); 609 | assertEquals(4, google.itemPosition); 610 | assertEquals(2, google.binderIndex); 611 | assertEquals(googleUri, google.item); 612 | 613 | final GraywaterAdapter.BinderResult five = adapter.computeItemAndBinderIndex(14); 614 | assertEquals(5, five.itemPosition); 615 | assertEquals(1, five.binderIndex); 616 | assertEquals("five", five.item); 617 | } 618 | 619 | @Test 620 | public void GraywaterAdapter_FirstVHPosition_FoundVHPosition() throws Exception { 621 | final TestAdapter adapter = new TestAdapter(); 622 | adapter.add("Testing"); 623 | final Uri tumblrUri = Uri.parse("https://www.tumblr.com"); 624 | adapter.add(tumblrUri); 625 | adapter.add("Testing"); 626 | final int imageViewHolderPosition = adapter.getFirstViewHolderPosition(1, TestAdapter.ImageViewHolder.class); 627 | assertEquals(2, imageViewHolderPosition); 628 | } 629 | 630 | @Test 631 | public void GraywaterAdapter_FirstVHPosition_DidNotFindVHPosition() throws Exception { 632 | final TestAdapter adapter = new TestAdapter(); 633 | adapter.add("Testing"); 634 | final Uri tumblrUri = Uri.parse("https://www.tumblr.com"); 635 | adapter.add(tumblrUri); 636 | adapter.add("Testing"); 637 | final int imageViewHolderPosition = adapter.getFirstViewHolderPosition(0, TestAdapter.ImageViewHolder.class); 638 | assertEquals(-1, imageViewHolderPosition); 639 | } 640 | 641 | @Test 642 | public void GraywaterAdapter_FirstVHPosition_InvalidItemPosition() throws Exception { 643 | final TestAdapter adapter = new TestAdapter(); 644 | adapter.add("Testing"); 645 | final Uri tumblrUri = Uri.parse("https://www.tumblr.com"); 646 | adapter.add(tumblrUri); 647 | adapter.add("Testing"); 648 | final int imageViewHolderPosition = adapter.getFirstViewHolderPosition(0xDEADBEEF, TestAdapter.ImageViewHolder.class); 649 | assertEquals(-1, imageViewHolderPosition); 650 | } 651 | } 652 | -------------------------------------------------------------------------------- /graywater/src/test/resources/robolectric.properties: -------------------------------------------------------------------------------- 1 | constants=com.tumblr.graywater.BuildConfig 2 | packageName=com.tumblr.graywater 3 | sdk=23 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':graywater' 2 | --------------------------------------------------------------------------------