├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASING.md ├── build.gradle ├── build.sh ├── config ├── hooks │ ├── install-git-hooks.gradle │ └── pre-commit ├── license │ └── spotless.license.java.txt ├── pmd │ ├── pmd-report.xslt │ └── pmd-ruleset.xml └── quality_android.gradle ├── feature-adapter-group ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── groupon │ │ │ └── featureadapter │ │ │ ├── GroupAdapterViewTypeDelegate.java │ │ │ └── GroupDiffUtilComparator.java │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── groupon │ └── featureadapter │ ├── GroupAdapterViewTypeDelegateTest.java │ └── StubAdapterViewTypeDelegate.java ├── feature-adapter-rx ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── groupon │ └── featureadapter │ ├── RxFeaturesAdapter.java │ └── events │ ├── FeatureControllerOnSubscribe.java │ └── RxFeatureEvent.java ├── feature-adapter-sample-rx ├── build.gradle ├── lint.xml ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── groupon │ │ └── android │ │ └── featureadapter │ │ └── sample │ │ ├── DealDetailsActivity.java │ │ ├── FeatureAnimatorModule.java │ │ ├── FeatureItemDecorationModule.java │ │ ├── events │ │ └── RefreshDealCommand.java │ │ ├── features │ │ ├── FeatureControllerListCreator.java │ │ ├── badges │ │ │ ├── BadgeAdapterViewTypeDelegate.java │ │ │ ├── BadgeController.java │ │ │ ├── BadgeModel.java │ │ │ ├── GroupBadgeAdapterViewTypeDelegate.java │ │ │ └── OnBadgeTap.java │ │ ├── collapsible │ │ │ ├── CollapsibleController.java │ │ │ ├── CollapsibleFeatureState.java │ │ │ ├── CollapsibleParentAdapterViewTypeDelegate.java │ │ │ ├── CollapsibleParentAnimatorListener.java │ │ │ ├── CollapsibleParentDiffUtilComparator.java │ │ │ ├── CollapsibleParentItemInfo.java │ │ │ ├── CollapsibleParentModel.java │ │ │ ├── CollapsibleParentViewHolder.java │ │ │ └── OnCollapsibleParentTap.java │ │ ├── header │ │ │ ├── HeaderController.java │ │ │ ├── ImageAdapterViewTypeDelegate.java │ │ │ └── TitleAdapterViewTypeDelegate.java │ │ └── options │ │ │ ├── OnOptionClickEvent.java │ │ │ ├── OptionsAdapterViewTypeDelegate.java │ │ │ ├── OptionsController.java │ │ │ ├── OptionsItemDecoration.java │ │ │ └── OptionsModel.java │ │ ├── model │ │ ├── Deal.java │ │ ├── DealApiClient.java │ │ └── Option.java │ │ └── state │ │ ├── DealDetailsScopeSingleton.java │ │ ├── SampleModel.java │ │ └── SampleStore.java │ └── res │ ├── drawable │ ├── divider.xml │ ├── ic_caret_up_black_24dp.xml │ ├── ic_keyboard_arrow_down_black_24px.xml │ ├── ic_keyboard_arrow_left_black_24px.xml │ ├── ic_keyboard_arrow_right_black_24px.xml │ ├── ic_keyboard_arrow_up_black_24px.xml │ └── ic_keyboard_capslock_black_24px.xml │ ├── layout │ ├── activity_with_recycler.xml │ ├── sample_badge.xml │ ├── sample_badge_group.xml │ ├── sample_collapsible_parent.xml │ ├── sample_header_image.xml │ ├── sample_header_title.xml │ └── sample_option.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── feature-adapter ├── build.gradle ├── gradle.properties └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── groupon │ │ └── featureadapter │ │ ├── AdapterViewTypeDelegate.java │ │ ├── DefaultDiffUtilComparator.java │ │ ├── DiffUtilCallbackImpl.java │ │ ├── DiffUtilComparator.java │ │ ├── FeatureAdapterDefaultAnimator.java │ │ ├── FeatureAdapterItemDecoration.java │ │ ├── FeatureAnimatorController.java │ │ ├── FeatureAnimatorListener.java │ │ ├── FeatureController.java │ │ ├── FeatureItemDecoration.java │ │ ├── FeatureItems.java │ │ ├── FeatureUpdate.java │ │ ├── FeaturesAdapter.java │ │ ├── FeaturesAdapterErrorHandler.java │ │ ├── ViewItem.java │ │ └── events │ │ ├── FeatureEvent.java │ │ ├── FeatureEventListener.java │ │ └── FeatureEventSource.java │ └── test │ └── java │ └── com │ └── groupon │ └── featureadapter │ ├── AdapterViewTypeDelegateTest.java │ ├── DefaultDiffUtilComparatorTest.java │ ├── FeatureControllerTest.java │ ├── FeaturesAdapterTest.java │ ├── StubAdapterViewTypeDelegate.java │ ├── StubFeatureController.java │ └── TestUtils.java ├── gradle.properties ├── gradle ├── gradle-mvn-push.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── design-overview.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | #IDE 15 | .idea 16 | *.iml 17 | 18 | #gradle 19 | build/ 20 | .gradle/ 21 | local.properties 22 | 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | android: 4 | components: 5 | # https://github.com/travis-ci/travis-ci/issues/5036 6 | - tools 7 | - build-tools-28.0.3 8 | - android-28 9 | - extra-android-m2repository 10 | 11 | jdk: 12 | - oraclejdk8 13 | 14 | #sonatype username and password 15 | branches: 16 | except: 17 | - gh-pages 18 | 19 | notifications: 20 | email: false 21 | 22 | sudo: false 23 | 24 | before_cache: 25 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 26 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 27 | 28 | cache: 29 | directories: 30 | - $HOME/.gradle/caches/ 31 | - $HOME/.gradle/wrapper/ 32 | - $HOME/.android/build-cache 33 | 34 | script: 35 | - ./gradlew check jacocoTestDebugUnitTestReport 36 | - ./gradlew check 37 | 38 | #after_success: 39 | - bash <(curl -s https://codecov.io/bash) 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### version 2.1.2 (TBD) 2 | 3 | ### version 2.1.1 (June 3rd, 2019) 4 | * Fixed issue #33: fixed NPE in non Rx adapter when a controller returns null (non updated feature) 5 | 6 | ### version 2.1.0 (April 26th, 2019) 7 | * Better preservation of internal back pressure strategy 8 | 9 | ### version 2.0.0 (March 5th, 2019) 10 | * migrate to Android X (no longer supports android support library) 11 | * upgrade to RxJava2 12 | 13 | ### version 1.0.13 (August 16th, 2018) 14 | move the view item check for issue #12 into FeatureAdapter class 15 | 16 | ### version 1.0.12 (August 7th, 2018) 17 | 18 | * add a check in case a view item is associated to a non registered view type delegate: 19 | https://github.com/groupon/FeatureAdapter/issues/12 20 | 21 | 22 | ### version 1.0.11 (April 13th, 2018) 23 | 24 | * initial release 25 | * solves issue #10: make animator controller thread safe. 26 | 27 | ### version 1.0.10 (29 Jan 2018) 28 | 29 | * fix an issue when the downstream is consuming the objects to fast and it doesn't wait for the result 30 | 31 | ### version 1.0.9 (25 Jan 2018) 32 | 33 | * limit the number of updates in rx feature adapter. Take only the latest object in a window of time provided by the feature controllers processing time. 34 | 35 | ### version 1.0.8 (23 Jan 2018) 36 | 37 | * fix multiple updates in the model. It was creating too many updates indeed. 38 | 39 | ### version 1.0.7 (23 Jan 2018) 40 | 41 | * fix multiple updates in the model 42 | * update the view item cache size so that all views are in the cache and they ain't recycled. 43 | 44 | ### version 1.0.6 (Dec. 14th 2017) 45 | 46 | * Fix view type init for group adapter: issues #43 47 | 48 | ### version 1.0.5 (Dec. 7th 2017) 49 | 50 | * Added GroupAdapterViewTypeDelegate module: issue #41 51 | 52 | ### version 1.0.4 (Dec. 6th, 2017) 53 | 54 | * Create a method in FeatureAdapter to get AdapterViewTypeDelegate for a certain ViewType: issue #39 55 | 56 | ### version 1.0.3 (Nov. 29th, 2017) 57 | 58 | * The adapter needs a method to return a view type delegate for a viewHolder object: issue #35 59 | * Create a way to add animators + item decorators for features: #34 60 | 61 | ### version 1.0.2 (Nov. 27th, 2017) 62 | 63 | * Make the sample more green beautiful: issue #13 64 | * Add an error handler to feature adapter instances to catch all throwables: #30 65 | * getFirstItemPositionForType should use a view type delegate not a int for the viewtype: #31 66 | 67 | ### version 1.0.1 (Oct. 30th, 2017) 68 | 69 | * Better comparator API: issue #25 70 | * modernize the build system: #26 71 | * payloads: #24 72 | * fix parallax bug: #22 73 | 74 | ### version 1.0.0 (August 8th, 2017) 75 | 76 | * initial release: 1.0.0 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## FeatureAdapter 2 | FeatureAdapter (FA) is an Android Library providing an optimized way to display complex screens on Android. 3 | 4 | [![Build Status](https://travis-ci.org/groupon/FeatureAdapter.svg?branch=master)](https://travis-ci.org/groupon/FeatureAdapter) 5 | [![codecov](https://codecov.io/gh/groupon/FeatureAdapter/branch/master/graph/badge.svg)](https://codecov.io/gh/groupon/FeatureAdapter) 6 | 7 | It offers a RecyclerView adapter that allows to define multiple “features”. Each “feature” contains multiple items rendered by multiple view types. 8 | 9 | Starting from version `2.x`, FeatureAdapter has been migrated to Android X and no longer supports android support library. If you want to use FeatureAdapter with android support library, use the version `1.y.z`. Version `2.x` are also based on Rx2, Rx1 is no longer supported. 10 | 11 | ## Design Overview 12 | FA will help to render a complex screen on android inside a RecyclerView. By complex, we mean a compound view that is composed of multiple items rendered by multiple view types. 13 | 14 |

15 | 16 |

17 | 18 | **Big Model:** 19 | 20 | The input of this screen is what we call “Big Model”, it’s a rich pojo that contains all (or most) of the information to render on the screen. We will render this “Big Model” inside a RecyclerView by splitting it into features. Each feature represents one aspect of the Big Model. 21 | 22 | **Feature Controllers:** 23 | 24 | To use FA, developers will have to define a list of FeatureControllers. The role of FeatureControllers is to isolate the business logic related to displaying one single feature. Each FeatureController will take the big model and produces a list of ViewItems. 25 | 26 | **ViewItems:** 27 | 28 | Each view item represents one portion of the complex screen (usually one row, but FA supports alternative layouts). Every view item consists of one small model (derived from the big model) and an AdapterViewTypeDelegate that is used to render the small pojo on screen. 29 | 30 | ## Highlights 31 | FeatureAdapter is highly optimized. It uses the lazy rendering capability of the RecyclerView to render only the features that are shown on screen. It ensures blazing fast rendering, especially the Rx based [FA module](./feature-adapter-rx) as it uses parallel computation in a background thread pool. 32 | 33 | FeatureAdapter not only displays features fast, it also updates them efficiently when the big model changes (FA uses DiffUtil). Only features that have changed are rendered again. 34 | 35 | FA is highly customizable. For instance you can use custom renderers and custom ViewItem comparators and custom layouts, for each feature. 36 | 37 | FA enforces separation and concerns by providing a full isolation of various features, allowing multiple teams to work on separate parts of the same screen 38 | 39 | FA supports user interactions: each ViewItem can send events (FeatureEvent). 40 | 41 | FA supports arbitrary complex grouping of ViewItems in a feature. GroupAdapterViewTypeDelegate will render multiple ViewItems on the same row. 42 | 43 | FA supports custom animations and decorators for each feature. 44 | 45 | ## Setup 46 | ``` 47 | // to use vanilla FeatureAdapter 48 | implementation 'com.groupon.android.feature-adapter:feature-adapter:x.y.z' 49 | // to use FeatureAdapter with Rx 50 | implementation 'com.groupon.android.feature-adapter:feature-adapter-rx:x.y.z' 51 | // to group features on the same row 52 | implementation 'com.groupon.android.feature-adapter:feature-adapter-group:x.y.z' 53 | ``` 54 | 55 | ## Alternatives to FA 56 | * [Epoxy](https://github.com/airbnb/epoxy) 57 | * SimpleAdapter 58 | 59 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ======== 3 | 4 | 1. Change the version in `gradle.properties` to a non-SNAPSHOT version. 5 | 2. Update the `CHANGELOG.md` for the impending release. 6 | 3. Update the `README.md` with the new version (if it applies). 7 | 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version) 8 | 5. `./gradlew clean uploadArchives` 9 | 6. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact. 10 | 7. `git tag -a X.Y.X -m "Version X.Y.Z"` (where X.Y.Z is the new version) 11 | 8. Update the `gradle.properties` to the next SNAPSHOT version. 12 | 9. `git commit -am "Prepare next development version."` 13 | 10. `git push && git push --tags` 14 | 15 | If step 5 or 6 fails, drop the Sonatype repo, fix the problem, commit, and start again at step 5. 16 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | apply from: rootProject.file('config/hooks/install-git-hooks.gradle') 3 | 4 | buildscript { 5 | ext.versions = ['minSdk' : 14, 6 | 'compileSdk' : 28, 7 | 'buildTools' : '28.0.3',] 8 | 9 | ext.deps = [javaxannotation: 'javax.annotation:javax.annotation-api:1.2', 10 | findbugs : 'com.google.code.findbugs:jsr305:3.0.2', 11 | junit : 'junit:junit:4.12', 12 | easymock : 'org.easymock:easymock:3.4', 13 | rxjava : 'io.reactivex.rxjava2:rxjava:2.2.7', 14 | rxandroid : 'io.reactivex.rxjava2:rxandroid:2.1.1', 15 | 16 | 'support' : ['compat': "androidx.core:core:1.0.1", 17 | 'design': "com.google.android.material:material:1.0.0", 18 | 'recyclerview': "androidx.recyclerview:recyclerview:1.0.0"], 19 | 'spotless' : 'com.diffplug.spotless:spotless-plugin-gradle:3.10.0', 20 | ] 21 | 22 | repositories { 23 | jcenter() 24 | google() 25 | maven { 26 | url "https://plugins.gradle.org/m2/" 27 | } 28 | } 29 | 30 | dependencies { 31 | classpath 'com.android.tools.build:gradle:3.3.0' 32 | classpath deps.spotless 33 | classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.7.1" 34 | } 35 | } 36 | 37 | allprojects { 38 | group = GROUP 39 | version = VERSION_NAME 40 | 41 | repositories { 42 | jcenter() 43 | google() 44 | } 45 | 46 | ext { 47 | // plugin for all (checkstyle, pmd and findbugs) 48 | quality_gradle_android_file = "config/quality_android.gradle" 49 | 50 | // config files 51 | pmd_rulesetFile = "${project.rootDir}/config/pmd/pmd-ruleset.xml" 52 | } 53 | } 54 | 55 | task clean(type: Delete) { 56 | delete rootProject.buildDir 57 | } 58 | 59 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # print every command 3 | set -x 4 | # make the script fail if any command fails 5 | set -e 6 | set -o pipefail 7 | 8 | # temporary solution to fix build tools issue 9 | export PATH=$PATH:$ANDROID_HOME/tools/bin/ 10 | BUILD_TOOLS_VERSION=$(grep buildTools build.gradle | cut -d\' -f4) 11 | yes | sdkmanager --update && sdkmanager "build-tools;${BUILD_TOOLS_VERSION}" 12 | 13 | 14 | echo "Building FeatureAdapter" 15 | ./gradlew clean build 16 | -------------------------------------------------------------------------------- /config/hooks/install-git-hooks.gradle: -------------------------------------------------------------------------------- 1 | task installPreCommitGitHook(type: Copy) { 2 | from new File(rootProject.rootDir, 'config/hooks/pre-commit') 3 | into { new File(rootProject.rootDir, '.git/hooks') } 4 | 5 | doFirst { 6 | println "Installing pre-commit hook" 7 | } 8 | 9 | doLast { 10 | Runtime.getRuntime().exec("chmod -R +x .git/hooks/pre-commit"); 11 | } 12 | } 13 | 14 | task build { 15 | dependsOn installPreCommitGitHook 16 | } 17 | -------------------------------------------------------------------------------- /config/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | ./gradlew spotlessJavaApply 2 | -------------------------------------------------------------------------------- /config/license/spotless.license.java.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Groupon Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /config/pmd/pmd-report.xslt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | http://doc.ece.uci.edu/cvs/viewcvs.cgi/Zen/packages/src/ 6 | 7 | 8 | 9 | 10 | PMD Report 11 | 24 | 25 | 26 |

PMD Report

27 |
28 |

Summary

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
FilesTotalPriority 1Priority 2Priority 3Priority 4Priority 5
49 |
50 | 51 | 52 | 53 |

54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
Begin LineDescription
?annotate=HEAD#
68 |
69 |
70 |

Generated by PMD on .

71 | 72 | 73 |
74 | 75 |
76 | -------------------------------------------------------------------------------- /config/pmd/pmd-ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | This ruleset checks my code for bad stuff 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /config/quality_android.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'net.ltgt.errorprone' 2 | apply plugin: 'pmd' 3 | apply plugin: 'com.diffplug.gradle.spotless' 4 | 5 | dependencies { 6 | errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") 7 | errorprone 'com.google.errorprone:error_prone_core:2.0.21' 8 | } 9 | 10 | pmd { 11 | toolVersion = '5.5.0' 12 | ruleSetFiles = files("${pmd_rulesetFile}") 13 | } 14 | 15 | task pmd(type: Pmd) { 16 | ruleSets = [] //"java-basic", "java-braces", "java-strings" 17 | ignoreFailures = false 18 | 19 | source "src" 20 | includes = ['**/*.java'] 21 | excludes = ['**/gen/**', 'src/test/**', '**/v2/db/**', '**/nst/**', '**/UpgradeManager*'] 22 | reports { 23 | xml.enabled = true 24 | html.enabled = true 25 | } 26 | } 27 | 28 | check.dependsOn 'pmd' 29 | 30 | spotless { 31 | java { 32 | licenseHeaderFile rootProject.file('config/license/spotless.license.java.txt') 33 | googleJavaFormat() // use a specific formatter for Java files 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /feature-adapter-group/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /feature-adapter-group/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.dicedmelon.gradle:jacoco-android:0.1.4' 9 | } 10 | } 11 | 12 | apply plugin: 'com.android.library' 13 | apply from: rootProject.file("${quality_gradle_android_file}") 14 | apply from: rootProject.file('gradle/gradle-mvn-push.gradle') 15 | apply plugin: 'jacoco-android' 16 | 17 | android { 18 | compileSdkVersion versions.compileSdk 19 | buildToolsVersion versions.buildTools 20 | defaultConfig { 21 | minSdkVersion versions.minSdk 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | 29 | testOptions { 30 | unitTests.returnDefaultValues = true 31 | } 32 | } 33 | 34 | dependencies { 35 | api project(':feature-adapter') 36 | implementation deps.findbugs 37 | implementation deps.javaxannotation 38 | 39 | api deps.support.compat 40 | api deps.support.recyclerview 41 | 42 | api deps.rxjava 43 | api deps.rxandroid 44 | 45 | testImplementation deps.junit 46 | testImplementation deps.easymock 47 | } 48 | -------------------------------------------------------------------------------- /feature-adapter-group/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=feature-adapter-group 2 | POM_NAME=Feature adapter group 3 | POM_DESCRIPTION='ViewGroups to align view holders horizontally' 4 | POM_PACKAGING='jar' -------------------------------------------------------------------------------- /feature-adapter-group/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /feature-adapter-group/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /feature-adapter-group/src/main/java/com/groupon/featureadapter/GroupAdapterViewTypeDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.featureadapter; 17 | 18 | import androidx.recyclerview.widget.DiffUtil; 19 | import androidx.recyclerview.widget.ListUpdateCallback; 20 | import androidx.recyclerview.widget.RecyclerView; 21 | import android.view.View; 22 | import android.view.ViewGroup; 23 | 24 | import java.util.ArrayDeque; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | import java.util.Queue; 28 | 29 | import static java.util.Collections.singletonList; 30 | 31 | /** 32 | * A wrapper AdapterViewTypeDelegate that wraps 1 to many child AdapterViewTypeDelegates. 33 | * Enables the child delegates to continue to use DiffUtilComparator and view Recycling by type. 34 | */ 35 | public abstract class GroupAdapterViewTypeDelegate extends AdapterViewTypeDelegate> { 36 | 37 | private final List childAdapterViewTypeDelegates = new ArrayList<>(); 38 | private final List childDiffUtilComparators = new ArrayList<>(); 39 | private final List> childViewHolderCache = new ArrayList<>(); 40 | 41 | private final List operations = new ArrayList<>(); // small memory optimisation 42 | 43 | public GroupAdapterViewTypeDelegate(List delegates) { 44 | childAdapterViewTypeDelegates.addAll(delegates); 45 | int childViewType = 0; 46 | for (AdapterViewTypeDelegate delegate : childAdapterViewTypeDelegates) { 47 | if (delegate.getViewType() != RecyclerView.INVALID_TYPE) { 48 | throw new IllegalStateException("Do not reuse AdapterViewTypeDelegate instances"); 49 | } 50 | delegate.setViewType(childViewType); 51 | childViewType++; 52 | delegate.addFeatureEventListener(this::fireEvent); 53 | childDiffUtilComparators.add(delegate.createDiffUtilComparator()); 54 | childViewHolderCache.add(new ArrayDeque<>()); 55 | } 56 | } 57 | 58 | protected abstract ViewGroup getRootViewGroup(HOLDER holder); 59 | 60 | @Override 61 | public void bindViewHolder(HOLDER holder, List viewItems) { 62 | unbindViewHolder(holder); 63 | for (ViewItem viewItem : viewItems) { 64 | final RecyclerView.ViewHolder childViewHolder = getChildViewHolder(viewItem.viewType, getRootViewGroup(holder)); 65 | final AdapterViewTypeDelegate delegate = childAdapterViewTypeDelegates.get(viewItem.viewType); 66 | delegate.bindViewHolder(childViewHolder, viewItem.model); 67 | setChildViewState(childViewHolder, viewItem); 68 | getRootViewGroup(holder).addView(childViewHolder.itemView); 69 | } 70 | } 71 | 72 | @Override 73 | public void bindViewHolder(HOLDER holder, List viewItems, List payloads) { 74 | if (payloads == null || payloads.isEmpty()) { 75 | bindViewHolder(holder, viewItems); 76 | return; 77 | } 78 | 79 | // Build list of operations to match old view items 80 | operations.clear(); 81 | ViewGroup rootViewGroup = getRootViewGroup(holder); 82 | for (int i = 0; i < rootViewGroup.getChildCount(); i++) { 83 | operations.add(Operation.nilOperation()); 84 | } 85 | 86 | // Iterate diff result instructions to update operations to match new view items 87 | DiffUtil.DiffResult diffResult = (DiffUtil.DiffResult) payloads.get(0); 88 | diffResult.dispatchUpdatesTo(new ListUpdateCallback() { 89 | @Override 90 | public void onInserted(int position, int count) { 91 | for (int i = position; i < position + count; i++) { 92 | operations.add(i, Operation.addOperation()); 93 | } 94 | } 95 | 96 | @Override 97 | public void onRemoved(int position, int count) { 98 | for (int i = position; i < position + count; i++) { 99 | operations.remove(position); 100 | unbindChildView(rootViewGroup.getChildAt(position)); 101 | rootViewGroup.removeViewAt(position); 102 | } 103 | } 104 | 105 | @Override 106 | public void onChanged(int position, int count, Object payload) { 107 | for (int i = position; i < position + count; i++) { 108 | operations.set(i, Operation.changeOperation(payload)); 109 | } 110 | } 111 | 112 | @Override 113 | public void onMoved(int fromPosition, int toPosition) { 114 | // not detected 115 | } 116 | }); 117 | 118 | // Operations now match index for index with new view items 119 | // Apply operations to old view state to bring up to date 120 | for (int i = 0; i < operations.size(); i++) { 121 | Operation operation = operations.get(i); 122 | ViewItem viewItem = viewItems.get(i); 123 | AdapterViewTypeDelegate delegate = childAdapterViewTypeDelegates.get(viewItem.viewType); 124 | switch (operation.type) { 125 | case Operation.ADD: 126 | RecyclerView.ViewHolder childViewHolder = getChildViewHolder(viewItem.viewType, rootViewGroup); 127 | delegate.bindViewHolder(childViewHolder, viewItem.model); 128 | rootViewGroup.addView(childViewHolder.itemView, i); 129 | setChildViewState(childViewHolder, viewItem); 130 | break; 131 | 132 | case Operation.CHANGE: 133 | ChildViewState childViewState = getChildViewState(rootViewGroup.getChildAt(i)); 134 | if (operation.payload != null) { 135 | delegate.bindViewHolder(childViewState.childViewHolder, viewItem.model, singletonList(operation.payload)); 136 | } else { 137 | delegate.bindViewHolder(childViewState.childViewHolder, viewItem.model); 138 | } 139 | setChildViewState(childViewState.childViewHolder, viewItem); 140 | break; 141 | } 142 | } 143 | } 144 | 145 | private void unbindChildView(View childItemView) { 146 | ChildViewState childViewState = getChildViewState(childItemView); 147 | AdapterViewTypeDelegate delegate = childAdapterViewTypeDelegates.get(childViewState.viewItem.viewType); 148 | delegate.unbindViewHolder(childViewState.childViewHolder); 149 | childViewHolderCache.get(childViewState.viewItem.viewType).offer(childViewState.childViewHolder); 150 | childViewState.clearViewState(); 151 | } 152 | 153 | @Override 154 | public void unbindViewHolder(HOLDER holder) { 155 | ViewGroup rootViewGroup = getRootViewGroup(holder); 156 | if (rootViewGroup.getChildCount() == 0) { 157 | return; 158 | } 159 | 160 | // clear / cache child views 161 | for (int i = 0; i < rootViewGroup.getChildCount(); i++) { 162 | unbindChildView(rootViewGroup.getChildAt(i)); 163 | } 164 | 165 | // clear the view holder 166 | rootViewGroup.removeAllViews(); 167 | } 168 | 169 | @Override 170 | public DiffUtilComparator> createDiffUtilComparator() { 171 | return new GroupDiffUtilComparator(childDiffUtilComparators); 172 | } 173 | 174 | private RecyclerView.ViewHolder getChildViewHolder(int viewType, ViewGroup parent) { 175 | final Queue viewHolders = childViewHolderCache.get(viewType); 176 | if (!viewHolders.isEmpty()) { 177 | return viewHolders.poll(); 178 | } 179 | RecyclerView.ViewHolder childViewHolder = childAdapterViewTypeDelegates.get(viewType).createViewHolder(parent); 180 | childViewHolder.itemView.setTag(new ChildViewState()); 181 | return childViewHolder; 182 | } 183 | 184 | private ChildViewState getChildViewState(View childItemView) { 185 | return (ChildViewState) childItemView.getTag(); 186 | } 187 | 188 | private void setChildViewState(RecyclerView.ViewHolder childViewHolder, ViewItem viewItem) { 189 | ChildViewState childViewState = getChildViewState(childViewHolder.itemView); 190 | childViewState.childViewHolder = childViewHolder; 191 | childViewState.viewItem = viewItem; 192 | } 193 | 194 | /** 195 | * View State stored against child views 196 | */ 197 | private static class ChildViewState { 198 | ViewItem viewItem; 199 | RecyclerView.ViewHolder childViewHolder; 200 | 201 | void clearViewState() { 202 | viewItem = null; 203 | childViewHolder = null; 204 | } 205 | } 206 | 207 | /** 208 | * We must buffer DiffUtil.DiffResult operations in order to be able to match an operation 209 | * to its ViewItem. 210 | */ 211 | private static class Operation { 212 | static final String NIL = "NIL"; 213 | static final String ADD = "ADD"; 214 | static final String CHANGE = "CHANGE"; 215 | 216 | private static final Operation NIL_INSTANCE = new Operation(NIL, null); 217 | private static final Operation ADD_INSTANCE = new Operation(ADD, null); 218 | private static final Operation FULL_CHANGE_INSTANCE = new Operation(CHANGE, null); 219 | 220 | final String type; 221 | final Object payload; 222 | 223 | static Operation nilOperation() { 224 | return NIL_INSTANCE; 225 | } 226 | 227 | static Operation addOperation() { 228 | return ADD_INSTANCE; 229 | } 230 | 231 | static Operation changeOperation(Object payload) { 232 | return payload == null ? FULL_CHANGE_INSTANCE : new Operation(CHANGE, payload); 233 | } 234 | 235 | private Operation(String type, Object payload) { 236 | this.type = type; 237 | this.payload = payload; 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /feature-adapter-group/src/main/java/com/groupon/featureadapter/GroupDiffUtilComparator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.featureadapter; 17 | 18 | import androidx.annotation.Nullable; 19 | import androidx.recyclerview.widget.DiffUtil; 20 | 21 | import java.util.List; 22 | 23 | class GroupDiffUtilComparator implements DiffUtilComparator> { 24 | 25 | private final List childDiffUtilComparators; 26 | 27 | GroupDiffUtilComparator(List childDiffUtilComparators) { 28 | this.childDiffUtilComparators = childDiffUtilComparators; 29 | } 30 | 31 | @Override 32 | public boolean areItemsTheSame(List oldModel, List newModel) { 33 | return true; // assume position doesn't move 34 | } 35 | 36 | @Override 37 | public boolean areContentsTheSame(List oldItems, List newItems) { 38 | if (oldItems.size() != newItems.size()) { 39 | return false; 40 | } 41 | for (int i = 0; i < oldItems.size(); i++) { 42 | final ViewItem oldItem = oldItems.get(i); 43 | final ViewItem newItem = newItems.get(i); 44 | if (oldItem.viewType != newItem.viewType) { 45 | return false; 46 | } 47 | final DiffUtilComparator comparator = childDiffUtilComparators.get(oldItem.viewType); 48 | if (!comparator.areItemsTheSame(oldItem.model, newItem.model)) { 49 | return false; 50 | } 51 | if (!comparator.areContentsTheSame(oldItem.model, newItem.model)) { 52 | return false; 53 | } 54 | } 55 | return true; 56 | } 57 | 58 | @Override 59 | public Object getChangePayload(List oldItems, List newItems) { 60 | final DiffUtilCallback callback = new DiffUtilCallback(oldItems, newItems); 61 | return DiffUtil.calculateDiff(callback, false); 62 | } 63 | 64 | private class DiffUtilCallback extends DiffUtil.Callback { 65 | 66 | private final List oldItems; 67 | private final List newItems; 68 | 69 | DiffUtilCallback(List oldItems, List newItems) { 70 | this.oldItems = oldItems; 71 | this.newItems = newItems; 72 | } 73 | 74 | @Override 75 | public int getOldListSize() { 76 | return oldItems.size(); 77 | } 78 | 79 | @Override 80 | public int getNewListSize() { 81 | return newItems.size(); 82 | } 83 | 84 | @Override 85 | public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 86 | final ViewItem oldItem = oldItems.get(oldItemPosition); 87 | final ViewItem newItem = newItems.get(newItemPosition); 88 | return oldItem.viewType == newItem.viewType && 89 | childDiffUtilComparators.get(oldItem.viewType).areItemsTheSame(oldItem.model, newItem.model); 90 | } 91 | 92 | @Override 93 | public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 94 | final ViewItem oldItem = oldItems.get(oldItemPosition); 95 | final ViewItem newItem = newItems.get(newItemPosition); 96 | return childDiffUtilComparators.get(oldItem.viewType).areContentsTheSame(oldItem.model, newItem.model); 97 | } 98 | 99 | @Nullable 100 | @Override 101 | public Object getChangePayload(int oldItemPosition, int newItemPosition) { 102 | final ViewItem oldItem = oldItems.get(oldItemPosition); 103 | final ViewItem newItem = newItems.get(newItemPosition); 104 | return childDiffUtilComparators.get(oldItem.viewType).getChangePayload(oldItem.model, newItem.model); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /feature-adapter-group/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | feature-adapter-group 3 | 4 | -------------------------------------------------------------------------------- /feature-adapter-group/src/test/java/com/groupon/featureadapter/StubAdapterViewTypeDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.featureadapter; 17 | 18 | import androidx.recyclerview.widget.RecyclerView.ViewHolder; 19 | import android.view.ViewGroup; 20 | 21 | class StubAdapterViewTypeDelegate extends AdapterViewTypeDelegate { 22 | @Override 23 | public ViewHolder createViewHolder(ViewGroup parent) { 24 | return null; 25 | } 26 | 27 | @Override 28 | public void bindViewHolder(ViewHolder holder, Object o) {} 29 | 30 | @Override 31 | public void unbindViewHolder(ViewHolder holder) {} 32 | } 33 | -------------------------------------------------------------------------------- /feature-adapter-rx/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.dicedmelon.gradle:jacoco-android:0.1.4' 9 | } 10 | } 11 | 12 | apply plugin: 'com.android.library' 13 | apply from: rootProject.file("${quality_gradle_android_file}") 14 | apply from: rootProject.file('gradle/gradle-mvn-push.gradle') 15 | apply plugin: 'jacoco-android' 16 | 17 | android { 18 | compileSdkVersion versions.compileSdk 19 | buildToolsVersion versions.buildTools 20 | defaultConfig { 21 | minSdkVersion versions.minSdk 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | 29 | testOptions { 30 | unitTests.returnDefaultValues = true 31 | } 32 | } 33 | 34 | dependencies { 35 | api project(':feature-adapter') 36 | implementation deps.findbugs 37 | implementation deps.javaxannotation 38 | 39 | api deps.support.compat 40 | api deps.support.design 41 | api deps.support.recyclerview 42 | 43 | api deps.rxjava 44 | api deps.rxandroid 45 | 46 | testImplementation deps.junit 47 | testImplementation deps.easymock 48 | } 49 | -------------------------------------------------------------------------------- /feature-adapter-rx/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=feature-adapter-rx 2 | POM_NAME=Feature adapter rx 3 | POM_DESCRIPTION='Rx 1 main artifact for Feature Adapter' 4 | POM_PACKAGING='jar' -------------------------------------------------------------------------------- /feature-adapter-rx/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /feature-adapter-rx/src/main/java/com/groupon/featureadapter/RxFeaturesAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.featureadapter; 17 | 18 | import androidx.recyclerview.widget.RecyclerView; 19 | import io.reactivex.BackpressureStrategy; 20 | import io.reactivex.Flowable; 21 | import io.reactivex.Observable; 22 | import io.reactivex.subjects.BehaviorSubject; 23 | import java.util.Comparator; 24 | import java.util.IdentityHashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | 28 | import static io.reactivex.Flowable.fromIterable; 29 | import static io.reactivex.Flowable.just; 30 | import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread; 31 | import static io.reactivex.schedulers.Schedulers.computation; 32 | 33 | public class RxFeaturesAdapter extends FeaturesAdapter { 34 | 35 | private final FeatureUpdateComparator featureUpdateComparator; 36 | private RecyclerView recyclerView; 37 | 38 | public RxFeaturesAdapter(List> featureControllers) { 39 | super(featureControllers); 40 | featureUpdateComparator = new FeatureUpdateComparator<>(getFeatureControllers()); 41 | } 42 | 43 | @Override 44 | public void onAttachedToRecyclerView(RecyclerView recyclerView) { 45 | super.onAttachedToRecyclerView(recyclerView); 46 | this.recyclerView = recyclerView; 47 | } 48 | 49 | @Override 50 | public void onDetachedFromRecyclerView(RecyclerView recyclerView) { 51 | super.onDetachedFromRecyclerView(recyclerView); 52 | this.recyclerView = null; 53 | } 54 | 55 | /** 56 | * Calculates each feature's new items and diff in parallel in the computation scheduler pool, 57 | * then dispatches feature updates to adapter in feature order. 58 | * 59 | * @param modelObservable the stream of models 60 | * @return an observable of {@link FeatureUpdate} for tracking the adapter changes. 61 | */ 62 | public Flowable> updateFeatureItems(Observable modelObservable) { 63 | // the ticker observable is gonna emit an item every time all the 64 | // list of items from all the feature controllers have been computed 65 | // so we just process the model instances one at a time 66 | // this is meant to be a very fine grained back pressure mechanism. 67 | final Object tick = new Object(); 68 | final BehaviorSubject tickObservable = BehaviorSubject.create(); 69 | tickObservable.onNext(tick); 70 | return modelObservable 71 | .observeOn(mainThread()) 72 | .zipWith(tickObservable, (model, t) -> model) 73 | .toFlowable(BackpressureStrategy.LATEST) 74 | .flatMap( 75 | model -> 76 | fromIterable(getFeatureControllers()) 77 | .flatMap( 78 | // each feature controller receives a fork of the model observable 79 | // and compute its items in parallel, and then updates the UI ASAP 80 | // but we still aggregate all the list to be sure to pace the model 81 | // observable 82 | // correctly using the tick observable 83 | feature -> 84 | just(feature) 85 | .observeOn(computation()) 86 | .map(featureController -> toFeatureUpdate(featureController, model)) 87 | .filter(featureUpdate -> featureUpdate.newItems != null && featureUpdate.diffResult != null) 88 | ) 89 | // collect all observable of feature updates in a list in feature order 90 | .toSortedList(featureUpdateComparator::compare) 91 | .observeOn(mainThread()) 92 | // dispatch each feature update in order to the adapter 93 | // (this also updates the internal adapter state) 94 | .map(this::dispatchFeatureUpdates) 95 | .map( 96 | list -> { 97 | tickObservable.onNext(tick); 98 | if (recyclerView != null) { 99 | recyclerView.setItemViewCacheSize(getItemCount()); 100 | } 101 | return list; 102 | }).toFlowable()); 103 | } 104 | 105 | private static class FeatureUpdateComparator implements Comparator { 106 | 107 | private final Map mapFeatureControllerToIndex = 108 | new IdentityHashMap<>(); 109 | 110 | FeatureUpdateComparator(List> featureControllers) { 111 | int idx = 0; 112 | for (FeatureController featureController : featureControllers) { 113 | mapFeatureControllerToIndex.put(featureController, idx++); 114 | } 115 | } 116 | 117 | @Override 118 | public int compare(FeatureUpdate o1, FeatureUpdate o2) { 119 | return mapFeatureControllerToIndex.get(o1.featureController) 120 | - mapFeatureControllerToIndex.get(o2.featureController); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /feature-adapter-rx/src/main/java/com/groupon/featureadapter/events/FeatureControllerOnSubscribe.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.featureadapter.events; 17 | 18 | import com.groupon.featureadapter.FeatureController; 19 | import io.reactivex.ObservableEmitter; 20 | import io.reactivex.ObservableOnSubscribe; 21 | 22 | import static io.reactivex.android.MainThreadDisposable.verifyMainThread; 23 | 24 | final class FeatureControllerOnSubscribe implements ObservableOnSubscribe { 25 | final FeatureController featureController; 26 | 27 | FeatureControllerOnSubscribe(FeatureController featureController) { 28 | this.featureController = featureController; 29 | } 30 | 31 | @Override 32 | public void subscribe(final ObservableEmitter subscriber) { 33 | verifyMainThread(); 34 | 35 | FeatureEventListener listener = 36 | event -> { 37 | if (!subscriber.isDisposed()) { 38 | subscriber.onNext(event); 39 | } 40 | }; 41 | 42 | subscriber.setCancellable(() -> featureController.removeFeatureEventListener(listener)); 43 | 44 | featureController.addFeatureEventListener(listener); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /feature-adapter-rx/src/main/java/com/groupon/featureadapter/events/RxFeatureEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.featureadapter.events; 17 | 18 | 19 | import com.groupon.featureadapter.FeatureController; 20 | import io.reactivex.Observable; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | import static io.reactivex.Observable.merge; 25 | 26 | /** 27 | * A helper class to make it easier to use {@link FeatureEvent}s from {@link FeatureController}s 28 | * with Rx 1. 29 | */ 30 | public class RxFeatureEvent { 31 | 32 | /** 33 | * Creates an observable of {@link FeatureEvent}s out of a {@link FeatureControllerGroup}. It is 34 | * possible to call this method multiple times on the controller. 35 | * 36 | *

Warning: The created observable keeps a strong reference to {@code 37 | * featureControllers}. Unsubscribe to free this reference. 38 | * 39 | * @param featureControllers a list of feature controllers. 40 | * @return an observable of the {@link FeatureEvent} that this group emits. 41 | */ 42 | public static Observable featureEvents( 43 | List> featureControllers) { 44 | List> observables = new ArrayList<>(); 45 | for (FeatureController controller : featureControllers) { 46 | observables.add(featureEvents(controller)); 47 | } 48 | return merge(observables); 49 | } 50 | 51 | /** 52 | * Creates an observable of {@link FeatureEvent}s out of a {@link FeatureController}. It is 53 | * possible to call this method multiple times on the controller. 54 | * 55 | *

Warning: The created observable keeps a strong reference to {@code controller}. 56 | * Unsubscribe to free this reference. 57 | * 58 | * @param controller a {@link FeatureController}. 59 | * @return an observable of the {@link FeatureEvent} that this controller emits. 60 | */ 61 | public static Observable featureEvents( 62 | FeatureController controller) { 63 | return Observable.create(new FeatureControllerOnSubscribe(controller)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion "28.0.3" 6 | defaultConfig { 7 | applicationId "com.groupon.android.featureadapter.sample.rx" 8 | minSdkVersion 19 9 | targetSdkVersion 26 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | compileOptions { 21 | sourceCompatibility JavaVersion.VERSION_1_8 22 | targetCompatibility JavaVersion.VERSION_1_8 23 | } 24 | 25 | lintOptions { 26 | abortOnError true 27 | quiet false 28 | htmlReport true 29 | xmlReport true 30 | lintConfig file("lint.xml") 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation project(':feature-adapter-rx') 36 | implementation project(':feature-adapter-group') 37 | 38 | implementation 'androidx.appcompat:appcompat:1.1.0-alpha02' 39 | implementation 'com.google.android.material:material:1.1.0-alpha03' 40 | implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha02' 41 | 42 | implementation 'io.reactivex.rxjava2:rxjava:2.2.7' 43 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' 44 | implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha2' 45 | 46 | implementation 'com.google.android:flexbox:1.1.0' 47 | 48 | implementation 'com.google.auto.value:auto-value-annotations:1.6.3' 49 | annotationProcessor 'com.google.auto.value:auto-value:1.6.3' 50 | implementation 'javax.annotation:javax.annotation-api:1.2' 51 | 52 | implementation 'com.jakewharton:butterknife:10.1.0' 53 | annotationProcessor 'com.jakewharton:butterknife-compiler:10.1.0' 54 | 55 | testImplementation 'junit:junit:4.12' 56 | testImplementation "org.easymock:easymock:3.4" 57 | compileOnly 'com.google.code.findbugs:jsr305:3.0.2' 58 | 59 | implementation 'com.groupon.grox:grox-core:1.1.2' 60 | implementation 'com.groupon.grox:grox-core-rx2:1.1.2' 61 | implementation 'com.groupon.grox:grox-commands-rx2:1.1.2' 62 | 63 | implementation 'com.github.stephanenicolas.toothpick:toothpick-runtime:2.1.0' 64 | implementation 'com.github.stephanenicolas.toothpick:smoothie-androidx:2.1.0' 65 | annotationProcessor 'com.github.stephanenicolas.toothpick:toothpick-compiler:2.1.0' 66 | 67 | testImplementation 'junit:junit:4.12' 68 | testImplementation 'org.easymock:easymock:3.4' 69 | } 70 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/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/ksmyth/Workspace/android/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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/DealDetailsActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample; 17 | 18 | import static com.groupon.android.featureadapter.sample.state.SampleModel.STATE_ERROR; 19 | import static com.groupon.android.featureadapter.sample.state.SampleModel.STATE_LOADING; 20 | import static com.groupon.android.featureadapter.sample.state.SampleModel.STATE_READY; 21 | import static com.groupon.featureadapter.events.RxFeatureEvent.featureEvents; 22 | import static com.groupon.grox.rxjava2.RxStores.states; 23 | import static com.jakewharton.rxbinding3.view.RxView.clicks; 24 | import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread; 25 | import static io.reactivex.schedulers.Schedulers.computation; 26 | import static io.reactivex.schedulers.Schedulers.io; 27 | import static toothpick.Toothpick.closeScope; 28 | import static toothpick.Toothpick.inject; 29 | import static toothpick.Toothpick.openScopes; 30 | 31 | import android.os.Bundle; 32 | import androidx.annotation.Nullable; 33 | import android.util.Log; 34 | import android.view.View; 35 | import android.widget.Button; 36 | import android.widget.ProgressBar; 37 | import androidx.appcompat.app.AppCompatActivity; 38 | import androidx.recyclerview.widget.RecyclerView; 39 | import butterknife.BindView; 40 | import butterknife.ButterKnife; 41 | import com.google.android.flexbox.FlexboxLayoutManager; 42 | import com.google.android.material.snackbar.Snackbar; 43 | import com.groupon.android.featureadapter.sample.events.RefreshDealCommand; 44 | import com.groupon.android.featureadapter.sample.features.FeatureControllerListCreator; 45 | import com.groupon.android.featureadapter.sample.rx.R; 46 | import com.groupon.android.featureadapter.sample.state.DealDetailsScopeSingleton; 47 | import com.groupon.android.featureadapter.sample.state.SampleModel; 48 | import com.groupon.android.featureadapter.sample.state.SampleStore; 49 | import com.groupon.featureadapter.FeatureAdapterDefaultAnimator; 50 | import com.groupon.featureadapter.FeatureAdapterItemDecoration; 51 | import com.groupon.featureadapter.FeatureAnimatorController; 52 | import com.groupon.featureadapter.FeatureController; 53 | import com.groupon.featureadapter.FeatureUpdate; 54 | import com.groupon.featureadapter.RxFeaturesAdapter; 55 | import com.groupon.grox.commands.rxjava2.Command; 56 | import io.reactivex.BackpressureStrategy; 57 | import io.reactivex.disposables.CompositeDisposable; 58 | import java.util.List; 59 | import javax.inject.Inject; 60 | import toothpick.Scope; 61 | import toothpick.smoothie.module.SmoothieActivityModule; 62 | import toothpick.smoothie.module.SmoothieAndroidXActivityModule; 63 | 64 | public class DealDetailsActivity extends AppCompatActivity { 65 | 66 | @BindView(R.id.recycler_view) 67 | RecyclerView recyclerView; 68 | 69 | @BindView(R.id.button_refresh) 70 | Button refreshButton; 71 | 72 | @BindView(R.id.progress) 73 | ProgressBar progressBar; 74 | 75 | @Inject SampleStore store; 76 | @Inject FeatureAnimatorController featureAnimatorController; 77 | @Inject FeatureAdapterItemDecoration featureAdapterItemDecoration; 78 | @Inject FeatureControllerListCreator featureControllerListCreator; 79 | 80 | private Scope scope; 81 | 82 | private final CompositeDisposable compositeDisposable = new CompositeDisposable(); 83 | 84 | @Override 85 | protected void onCreate(@Nullable Bundle savedInstanceState) { 86 | scope = openScopes(getApplication(), DealDetailsScopeSingleton.class, this); 87 | scope.installModules( 88 | new SmoothieActivityModule(this), 89 | new SmoothieAndroidXActivityModule(this), 90 | new FeatureAnimatorModule(), 91 | new FeatureItemDecorationModule()); 92 | inject(this, scope); 93 | super.onCreate(savedInstanceState); 94 | setContentView(R.layout.activity_with_recycler); 95 | ButterKnife.bind(this); 96 | 97 | List> features = 98 | featureControllerListCreator.getFeatureControllerList(); 99 | RxFeaturesAdapter adapter = new RxFeaturesAdapter<>(features); 100 | 101 | recyclerView.setHasFixedSize(true); 102 | recyclerView.setLayoutManager(new FlexboxLayoutManager(this)); 103 | recyclerView.setAdapter(adapter); 104 | recyclerView.setItemAnimator(new FeatureAdapterDefaultAnimator(featureAnimatorController)); 105 | recyclerView.addItemDecoration(featureAdapterItemDecoration); 106 | 107 | compositeDisposable.add(clicks(refreshButton).subscribe(v -> refreshDeal(), this::logError)); 108 | 109 | refreshButton.setOnClickListener(ignored -> refreshDeal()); 110 | 111 | // listen for feature events 112 | compositeDisposable.add( 113 | featureEvents(features) 114 | .observeOn(computation()) 115 | // Grox and Feature Adapter are different libraries 116 | // to combine the 2, we need a mechanism that, given a feature event, 117 | // we trigger a command. In our sample, and we recommend it as a good practice, 118 | // our Grox commands implement the FeatureEvent interface. 119 | // This is why, the cast below is required 120 | .cast(Command.class) 121 | .flatMap(Command::actions) 122 | .subscribe(store::dispatch, this::logError)); 123 | 124 | // propagate states to features 125 | compositeDisposable.add( 126 | states(store) 127 | .subscribeOn(computation()) 128 | .to(adapter::updateFeatureItems) 129 | .subscribe(this::logFeatureUpdate, this::logError)); 130 | 131 | // listen for new states 132 | compositeDisposable.add( 133 | states(store).observeOn(mainThread()).subscribe(this::reactToNewState, this::logError)); 134 | 135 | if (store.getState().deal() == null) { 136 | refreshDeal(); 137 | } 138 | } 139 | 140 | @Override 141 | protected void onDestroy() { 142 | compositeDisposable.dispose(); 143 | super.onDestroy(); 144 | if (isFinishing()) { 145 | closeScope(DealDetailsScopeSingleton.class); 146 | } 147 | closeScope(this); 148 | } 149 | 150 | private void refreshDeal() { 151 | compositeDisposable.add( 152 | new RefreshDealCommand(scope) 153 | .actions() 154 | .subscribeOn(io()) 155 | .subscribe(store::dispatch, this::logError)); 156 | } 157 | 158 | private void reactToNewState(SampleModel sampleModel) { 159 | // update activity state 160 | switch (sampleModel.state()) { 161 | case STATE_READY: 162 | progressBar.setVisibility(View.GONE); 163 | break; 164 | case STATE_LOADING: 165 | progressBar.setVisibility(View.VISIBLE); 166 | break; 167 | case STATE_ERROR: 168 | progressBar.setVisibility(View.GONE); 169 | Snackbar.make(recyclerView, sampleModel.exceptionText(), Snackbar.LENGTH_LONG).show(); 170 | break; 171 | } 172 | } 173 | 174 | private void logFeatureUpdate(List featureUpdate) { 175 | Log.d(getClass().getSimpleName(), featureUpdate.toString()); 176 | } 177 | 178 | private void logError(Throwable t) { 179 | Log.e(getClass().getSimpleName(), t.getLocalizedMessage(), t); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/FeatureAnimatorModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample; 17 | 18 | import com.groupon.featureadapter.FeatureAnimatorController; 19 | 20 | import toothpick.config.Module; 21 | 22 | /** 23 | * Provides a shared instance of {@link FeatureAnimatorController} to pass into the RecyclerView 24 | * animator, and to inject into the Features themselves. 25 | */ 26 | class FeatureAnimatorModule extends Module { 27 | FeatureAnimatorModule() { 28 | bind(FeatureAnimatorController.class).toInstance(new FeatureAnimatorController()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/FeatureItemDecorationModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample; 17 | 18 | import com.groupon.featureadapter.FeatureAdapterItemDecoration; 19 | 20 | import toothpick.config.Module; 21 | 22 | /** 23 | * Provides a shared instance of {@link FeatureAdapterItemDecoration} to pass to the RecyclerView 24 | * and to inject into the Features themselves. 25 | */ 26 | class FeatureItemDecorationModule extends Module { 27 | FeatureItemDecorationModule() { 28 | bind(FeatureAdapterItemDecoration.class).toInstance(new FeatureAdapterItemDecoration()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/events/RefreshDealCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.events; 17 | 18 | import com.groupon.android.featureadapter.sample.model.Deal; 19 | import com.groupon.android.featureadapter.sample.model.DealApiClient; 20 | import com.groupon.android.featureadapter.sample.model.Option; 21 | import com.groupon.android.featureadapter.sample.state.SampleModel; 22 | import com.groupon.featureadapter.events.FeatureEvent; 23 | import com.groupon.grox.Action; 24 | 25 | import com.groupon.grox.commands.rxjava2.Command; 26 | import io.reactivex.Observable; 27 | import javax.inject.Inject; 28 | 29 | import toothpick.Scope; 30 | 31 | import static com.groupon.android.featureadapter.sample.state.SampleModel.STATE_ERROR; 32 | import static com.groupon.android.featureadapter.sample.state.SampleModel.STATE_LOADING; 33 | import static com.groupon.android.featureadapter.sample.state.SampleModel.STATE_READY; 34 | import static toothpick.Toothpick.inject; 35 | 36 | /** 37 | * Encapsulate the state update logic to refresh a deal from the Api. 38 | * This class implements {@link FeatureEvent} so that it can be launched from features. 39 | * It also implements {@link Command} so that it can be processed in a Grox chain. 40 | */ 41 | public class RefreshDealCommand implements Command, FeatureEvent { 42 | 43 | @Inject DealApiClient dealApiClient; 44 | 45 | public RefreshDealCommand(Scope scope) { 46 | inject(this, scope); 47 | } 48 | 49 | @Override 50 | public Observable> actions() { 51 | return dealApiClient.getDeal() 52 | .map(SuccessAction::new) 53 | .map(action -> (Action) action) 54 | .onErrorReturn(FailedAction::new) 55 | .startWith(new StateLoadingAction()); 56 | } 57 | 58 | /** 59 | * The deal is fetched successfully. 60 | */ 61 | private static class SuccessAction implements Action { 62 | 63 | private final Deal deal; 64 | 65 | SuccessAction(Deal deal) { 66 | this.deal = deal; 67 | } 68 | 69 | @Override 70 | public SampleModel newState(SampleModel oldState) { 71 | return oldState.toBuilder() 72 | .setDeal(deal) 73 | .setSelectedOption(updateOption(deal, oldState.selectedOption())) 74 | .setState(STATE_READY) 75 | .setExceptionText(null) 76 | .build(); 77 | } 78 | 79 | private static Option updateOption(Deal deal, Option previousOption) { 80 | if (previousOption == null) return null; 81 | for (Option option : deal.options) { 82 | if (previousOption.uuid.equals(option.uuid)) return option; 83 | } 84 | throw new IllegalArgumentException("Option does not exist"); 85 | } 86 | } 87 | 88 | /** 89 | * There is an exception while fetching the deal. 90 | */ 91 | private static class FailedAction implements Action { 92 | 93 | private final Throwable throwable; 94 | 95 | FailedAction(Throwable throwable) { 96 | this.throwable = throwable; 97 | } 98 | 99 | @Override 100 | public SampleModel newState(SampleModel oldState) { 101 | return oldState.toBuilder() 102 | .setState(STATE_ERROR) 103 | .setExceptionText(throwable.getLocalizedMessage()) 104 | .build(); 105 | } 106 | } 107 | 108 | /** 109 | * Update the progress state when fetching the deal. 110 | */ 111 | private static class StateLoadingAction implements Action { 112 | 113 | @Override 114 | public SampleModel newState(SampleModel oldState) { 115 | return oldState.toBuilder() 116 | .setState(STATE_LOADING) 117 | .build(); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/FeatureControllerListCreator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features; 17 | 18 | import com.groupon.android.featureadapter.sample.features.badges.BadgeController; 19 | import com.groupon.android.featureadapter.sample.features.collapsible.CollapsibleController; 20 | import com.groupon.android.featureadapter.sample.features.header.HeaderController; 21 | import com.groupon.android.featureadapter.sample.features.options.OptionsController; 22 | import com.groupon.android.featureadapter.sample.state.SampleModel; 23 | import com.groupon.featureadapter.FeatureController; 24 | 25 | import java.util.List; 26 | 27 | import javax.inject.Inject; 28 | 29 | import static java.util.Arrays.asList; 30 | 31 | public class FeatureControllerListCreator { 32 | 33 | private final List> featureControllers; 34 | 35 | @Inject 36 | public FeatureControllerListCreator(OptionsController optionsController, 37 | CollapsibleController collapsibleController) { 38 | featureControllers = asList( 39 | new HeaderController(), 40 | optionsController, 41 | collapsibleController, 42 | new BadgeController() 43 | ); 44 | } 45 | 46 | public List> getFeatureControllerList() { 47 | return featureControllers; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/badges/BadgeAdapterViewTypeDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.badges; 17 | 18 | import androidx.recyclerview.widget.RecyclerView; 19 | import android.view.LayoutInflater; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.widget.TextView; 23 | 24 | import com.groupon.android.featureadapter.sample.rx.R; 25 | import com.groupon.featureadapter.AdapterViewTypeDelegate; 26 | import com.groupon.featureadapter.DiffUtilComparator; 27 | 28 | import java.util.List; 29 | 30 | import butterknife.BindView; 31 | import butterknife.ButterKnife; 32 | 33 | class BadgeAdapterViewTypeDelegate extends AdapterViewTypeDelegate { 34 | 35 | private static final int LAYOUT = R.layout.sample_badge; 36 | 37 | @Override 38 | public ViewHolder createViewHolder(ViewGroup parent) { 39 | return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(LAYOUT, parent, false)); 40 | } 41 | 42 | @Override 43 | public void bindViewHolder(ViewHolder holder, BadgeModel model) { 44 | holder.badgeText.setText(model.badgeText); 45 | holder.badgeText.setAllCaps(model.isHighlighted); 46 | holder.badgeText.setOnClickListener(v -> fireEvent(new OnBadgeTap(model.badgeText))); 47 | } 48 | 49 | @Override 50 | public void bindViewHolder(ViewHolder holder, BadgeModel model, List payloads) { 51 | if (payloads == null || payloads.isEmpty()) { 52 | bindViewHolder(holder, model); 53 | return; 54 | } 55 | // only highlighted has been modified 56 | holder.badgeText.setAllCaps(model.isHighlighted); 57 | } 58 | 59 | @Override 60 | public void unbindViewHolder(ViewHolder holder) { 61 | // no op 62 | } 63 | 64 | @Override 65 | public DiffUtilComparator createDiffUtilComparator() { 66 | return new DiffUtilComparator() { 67 | 68 | @Override 69 | public boolean areItemsTheSame(BadgeModel oldModel, BadgeModel newModel) { 70 | return oldModel.badgeText.equals(newModel.badgeText); 71 | } 72 | 73 | @Override 74 | public boolean areContentsTheSame(BadgeModel oldModel, BadgeModel newModel) { 75 | return oldModel.isHighlighted == newModel.isHighlighted; 76 | } 77 | 78 | @Override 79 | public Object getChangePayload(BadgeModel oldModel, BadgeModel newModel) { 80 | return "isHighlighted"; 81 | } 82 | }; 83 | } 84 | 85 | static class ViewHolder extends RecyclerView.ViewHolder { 86 | 87 | @BindView(R.id.badge_text) TextView badgeText; 88 | 89 | ViewHolder(View itemView) { 90 | super(itemView); 91 | ButterKnife.bind(this, itemView); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/badges/BadgeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.badges; 17 | 18 | import com.groupon.android.featureadapter.sample.model.Deal; 19 | import com.groupon.android.featureadapter.sample.state.SampleModel; 20 | import com.groupon.featureadapter.AdapterViewTypeDelegate; 21 | import com.groupon.featureadapter.FeatureController; 22 | import com.groupon.featureadapter.ViewItem; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Collection; 26 | import java.util.List; 27 | 28 | import static java.util.Arrays.asList; 29 | import static java.util.Collections.emptyList; 30 | import static java.util.Collections.singletonList; 31 | 32 | public class BadgeController extends FeatureController { 33 | 34 | private final BadgeAdapterViewTypeDelegate badgeDelegate = new BadgeAdapterViewTypeDelegate(); 35 | private final BadgeAdapterViewTypeDelegate childBadgeDelegate = new BadgeAdapterViewTypeDelegate(); 36 | private final GroupBadgeAdapterViewTypeDelegate groupBadgeDelegate = new GroupBadgeAdapterViewTypeDelegate(singletonList(childBadgeDelegate)); 37 | 38 | @Override 39 | public Collection getAdapterViewTypeDelegates() { 40 | return asList(badgeDelegate, groupBadgeDelegate); 41 | } 42 | 43 | @Override 44 | public List buildItems(SampleModel sampleModel) { 45 | Deal deal = sampleModel.deal(); 46 | if (deal == null) { 47 | return emptyList(); 48 | } 49 | List items = new ArrayList<>(); 50 | 51 | /* 52 | The first group of badges utilizes the {@link com.google.android.flexbox.FlexboxLayoutManager} 53 | to layout a wrapping list of badges in line with the rest of the Activity. 54 | */ 55 | for (String badge : deal.badges) { 56 | items.add(new ViewItem<>(new BadgeModel(badge, badge.equals(sampleModel.highlightedBadge())), badgeDelegate)); 57 | } 58 | 59 | /* 60 | The second group of badges utilizes the {@link GroupAdapterViewTypeDelegate} to nest a group 61 | of badges inside a custom layout. Note that it uses the same {@link BadgeAdapterViewTypeDelegate} 62 | class/ Note the same DiffUtilComparator and fireEvent still work for the child view items. 63 | */ 64 | List childItems = new ArrayList<>(); 65 | for (String badge : deal.badges) { 66 | childItems.add(new ViewItem<>(new BadgeModel(badge, badge.equals(sampleModel.highlightedBadge())), childBadgeDelegate)); 67 | } 68 | items.add(new ViewItem<>(childItems, groupBadgeDelegate)); 69 | 70 | return items; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/badges/BadgeModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.badges; 17 | 18 | class BadgeModel { 19 | final String badgeText; 20 | final boolean isHighlighted; 21 | 22 | BadgeModel(String badgeText, boolean isHighlighted) { 23 | this.badgeText = badgeText; 24 | this.isHighlighted = isHighlighted; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/badges/GroupBadgeAdapterViewTypeDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.badges; 17 | 18 | import androidx.recyclerview.widget.RecyclerView; 19 | import android.view.LayoutInflater; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.widget.LinearLayout; 23 | 24 | import com.groupon.android.featureadapter.sample.rx.R; 25 | import com.groupon.featureadapter.AdapterViewTypeDelegate; 26 | import com.groupon.featureadapter.GroupAdapterViewTypeDelegate; 27 | 28 | import java.util.List; 29 | 30 | import butterknife.BindView; 31 | import butterknife.ButterKnife; 32 | 33 | class GroupBadgeAdapterViewTypeDelegate extends GroupAdapterViewTypeDelegate { 34 | 35 | private static final int LAYOUT = R.layout.sample_badge_group; 36 | 37 | GroupBadgeAdapterViewTypeDelegate(List delegates) { 38 | super(delegates); 39 | } 40 | 41 | @Override 42 | public GroupBadgeViewHolder createViewHolder(ViewGroup parent) { 43 | return new GroupBadgeViewHolder(LayoutInflater.from(parent.getContext()).inflate(LAYOUT, parent, false)); 44 | } 45 | 46 | @Override 47 | protected ViewGroup getRootViewGroup(GroupBadgeViewHolder holder) { 48 | return holder.linearLayout; 49 | } 50 | 51 | static class GroupBadgeViewHolder extends RecyclerView.ViewHolder { 52 | 53 | @BindView(R.id.content_layout) LinearLayout linearLayout; 54 | 55 | GroupBadgeViewHolder(View itemView) { 56 | super(itemView); 57 | ButterKnife.bind(this, itemView); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/badges/OnBadgeTap.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.badges; 17 | 18 | import com.groupon.android.featureadapter.sample.state.SampleModel; 19 | import com.groupon.featureadapter.events.FeatureEvent; 20 | import com.groupon.grox.commands.rxjava2.SingleActionCommand; 21 | 22 | class OnBadgeTap extends SingleActionCommand implements FeatureEvent { 23 | 24 | private final String badge; 25 | 26 | OnBadgeTap(String badge) { 27 | this.badge = badge; 28 | } 29 | 30 | @Override 31 | public SampleModel newState(SampleModel oldState) { 32 | String newHighlightedBadge = badge.equals(oldState.highlightedBadge()) ? null : badge; 33 | return oldState 34 | .toBuilder() 35 | .setHighlightedBadge(newHighlightedBadge) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/collapsible/CollapsibleController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.collapsible; 17 | 18 | import com.groupon.android.featureadapter.sample.state.SampleModel; 19 | import com.groupon.featureadapter.AdapterViewTypeDelegate; 20 | import com.groupon.featureadapter.FeatureAnimatorController; 21 | import com.groupon.featureadapter.FeatureController; 22 | import com.groupon.featureadapter.ViewItem; 23 | 24 | import java.util.Collection; 25 | import java.util.Collections; 26 | import java.util.List; 27 | 28 | import javax.inject.Inject; 29 | 30 | public class CollapsibleController extends FeatureController { 31 | 32 | private static final String TITLE = "Animating Collapsible Feature"; 33 | 34 | @Inject FeatureAnimatorController featureAnimatorController; 35 | 36 | private CollapsibleParentAnimatorListener parentAnimatorListener; 37 | 38 | private final CollapsibleParentAdapterViewTypeDelegate parentDelegate = new CollapsibleParentAdapterViewTypeDelegate(); 39 | 40 | @Inject 41 | public CollapsibleController() { 42 | } 43 | 44 | @Override 45 | public Collection getAdapterViewTypeDelegates() { 46 | return Collections.singletonList(parentDelegate); 47 | } 48 | 49 | @Override 50 | public List buildItems(SampleModel sampleModel) { 51 | if (sampleModel.deal() == null) { 52 | return Collections.emptyList(); 53 | } 54 | 55 | if (parentAnimatorListener == null) { 56 | // register animator 57 | parentAnimatorListener = new CollapsibleParentAnimatorListener(); 58 | featureAnimatorController.registerFeatureAnimatorListener(parentAnimatorListener, parentDelegate); 59 | } 60 | 61 | final CollapsibleParentModel parentModel = new CollapsibleParentModel(TITLE, sampleModel.collapsibleFeatureState().isCollapsed); 62 | return Collections.singletonList(new ViewItem<>(parentModel, parentDelegate)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/collapsible/CollapsibleFeatureState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.collapsible; 17 | 18 | public class CollapsibleFeatureState { 19 | 20 | public static final CollapsibleFeatureState DEFAULT = new CollapsibleFeatureState(false); 21 | 22 | public final boolean isCollapsed; 23 | 24 | public CollapsibleFeatureState(boolean isCollapsed) { 25 | this.isCollapsed = isCollapsed; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/collapsible/CollapsibleParentAdapterViewTypeDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.collapsible; 17 | 18 | import android.view.LayoutInflater; 19 | import android.view.ViewGroup; 20 | 21 | import com.groupon.android.featureadapter.sample.rx.R; 22 | import com.groupon.featureadapter.AdapterViewTypeDelegate; 23 | import com.groupon.featureadapter.DiffUtilComparator; 24 | 25 | import java.util.List; 26 | 27 | class CollapsibleParentAdapterViewTypeDelegate extends AdapterViewTypeDelegate { 28 | 29 | static final float CARET_ROTATION_EXPANDED = 0f; 30 | static final float CARET_ROTATION_COLLAPSED = 180f; 31 | 32 | private static final int LAYOUT = R.layout.sample_collapsible_parent; 33 | 34 | @Override 35 | public CollapsibleParentViewHolder createViewHolder(ViewGroup parent) { 36 | return new CollapsibleParentViewHolder(LayoutInflater.from(parent.getContext()).inflate(LAYOUT, parent, false)); 37 | } 38 | 39 | @Override 40 | public void bindViewHolder(CollapsibleParentViewHolder holder, CollapsibleParentModel model) { 41 | holder.titleText.setText(model.title); 42 | holder.caretImage.setRotation(model.isCollapsed ? CARET_ROTATION_COLLAPSED : CARET_ROTATION_EXPANDED); 43 | holder.itemView.setOnClickListener(v -> fireEvent(new OnCollapsibleParentTap())); 44 | holder.model = model; 45 | } 46 | 47 | @Override 48 | public void bindViewHolder(CollapsibleParentViewHolder holder, CollapsibleParentModel model, List payloads) { 49 | if (payloads == null || payloads.isEmpty()) { 50 | bindViewHolder(holder, model); 51 | } 52 | // This point will be reached if the DiffUtilComparator returns a payload for the collapsed 53 | // value updating. Caret rotation will be handled by the animator 54 | holder.model = model; 55 | } 56 | 57 | @Override 58 | public void unbindViewHolder(CollapsibleParentViewHolder holder) { 59 | // no op 60 | } 61 | 62 | @Override 63 | public DiffUtilComparator createDiffUtilComparator() { 64 | return new CollapsibleParentDiffUtilComparator(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/collapsible/CollapsibleParentAnimatorListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.collapsible; 17 | 18 | import android.animation.Animator; 19 | import android.animation.AnimatorListenerAdapter; 20 | import android.animation.ObjectAnimator; 21 | import androidx.recyclerview.widget.RecyclerView; 22 | import android.widget.ImageView; 23 | 24 | import com.groupon.featureadapter.FeatureAnimatorListener; 25 | 26 | import static android.view.View.ROTATION; 27 | import static com.groupon.android.featureadapter.sample.features.collapsible.CollapsibleParentAdapterViewTypeDelegate.CARET_ROTATION_COLLAPSED; 28 | import static com.groupon.android.featureadapter.sample.features.collapsible.CollapsibleParentAdapterViewTypeDelegate.CARET_ROTATION_EXPANDED; 29 | 30 | class CollapsibleParentAnimatorListener implements FeatureAnimatorListener { 31 | 32 | @Override 33 | public CollapsibleParentItemInfo getPreLayoutInformation(CollapsibleParentViewHolder viewHolder) { 34 | return new CollapsibleParentItemInfo(viewHolder); 35 | } 36 | 37 | @Override 38 | public CollapsibleParentItemInfo getPostLayoutInformation(CollapsibleParentViewHolder viewHolder) { 39 | return new CollapsibleParentItemInfo(viewHolder); 40 | } 41 | 42 | @Override 43 | public Animator setupChangeAnimation(RecyclerView.ItemAnimator itemAnimator, CollapsibleParentViewHolder oldHolder, CollapsibleParentViewHolder newHolder, CollapsibleParentItemInfo preInfo, CollapsibleParentItemInfo postInfo) { 44 | if (preInfo.isCollapsed == postInfo.isCollapsed) { 45 | return null; 46 | } 47 | final float rotationFrom = preInfo.isCollapsed ? CARET_ROTATION_COLLAPSED : CARET_ROTATION_EXPANDED; 48 | final float rotationTo = !preInfo.isCollapsed ? CARET_ROTATION_COLLAPSED : CARET_ROTATION_EXPANDED; 49 | final ObjectAnimator animation = ObjectAnimator.ofFloat(newHolder.caretImage, ROTATION.getName(), rotationFrom, rotationTo); 50 | animation.addListener(new OnAnimationFinishListener(newHolder.caretImage, rotationTo)); 51 | return animation; 52 | } 53 | 54 | private static class OnAnimationFinishListener extends AnimatorListenerAdapter { 55 | 56 | private boolean isCancelled; 57 | 58 | private final ImageView caretImage; 59 | private final float rotationTo; 60 | 61 | OnAnimationFinishListener(ImageView caretImage, float rotationTo) { 62 | this.caretImage = caretImage; 63 | this.rotationTo = rotationTo; 64 | } 65 | 66 | @Override 67 | public void onAnimationCancel(Animator animation) { 68 | isCancelled = true; 69 | } 70 | 71 | @Override 72 | public void onAnimationEnd(Animator animation) { 73 | if (!isCancelled) { 74 | caretImage.setRotation(rotationTo); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/collapsible/CollapsibleParentDiffUtilComparator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.collapsible; 17 | 18 | import com.groupon.featureadapter.DiffUtilComparator; 19 | 20 | class CollapsibleParentDiffUtilComparator implements DiffUtilComparator { 21 | 22 | @Override 23 | public boolean areItemsTheSame(CollapsibleParentModel oldModel, CollapsibleParentModel newModel) { 24 | return true; 25 | } 26 | 27 | @Override 28 | public boolean areContentsTheSame(CollapsibleParentModel oldModel, CollapsibleParentModel newModel) { 29 | return oldModel.isCollapsed == newModel.isCollapsed; 30 | } 31 | 32 | @Override 33 | public Object getChangePayload(CollapsibleParentModel oldModel, CollapsibleParentModel newModel) { 34 | // on reaching this point we know that isCollapsed has changed, so return a payload to 35 | // avoid doing a full bind 36 | return newModel.isCollapsed; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/collapsible/CollapsibleParentItemInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.collapsible; 17 | 18 | import androidx.recyclerview.widget.RecyclerView; 19 | 20 | class CollapsibleParentItemInfo extends RecyclerView.ItemAnimator.ItemHolderInfo { 21 | 22 | final boolean isCollapsed; 23 | 24 | CollapsibleParentItemInfo(CollapsibleParentViewHolder holder) { 25 | this.isCollapsed = holder.model.isCollapsed; 26 | setFrom(holder); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/collapsible/CollapsibleParentModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.collapsible; 17 | 18 | class CollapsibleParentModel { 19 | final String title; 20 | final boolean isCollapsed; 21 | 22 | CollapsibleParentModel(String title, boolean isCollapsed) { 23 | this.title = title; 24 | this.isCollapsed = isCollapsed; 25 | } 26 | 27 | CollapsibleParentModel withCollapsed(boolean isCollapsed) { 28 | return new CollapsibleParentModel(title, isCollapsed); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/collapsible/CollapsibleParentViewHolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.collapsible; 17 | 18 | import androidx.recyclerview.widget.RecyclerView; 19 | import android.view.View; 20 | import android.widget.ImageView; 21 | import android.widget.TextView; 22 | 23 | import com.groupon.android.featureadapter.sample.rx.R; 24 | 25 | import butterknife.BindView; 26 | import butterknife.ButterKnife; 27 | 28 | class CollapsibleParentViewHolder extends RecyclerView.ViewHolder { 29 | 30 | @BindView(R.id.collapsible_title_text) TextView titleText; 31 | @BindView(R.id.collapsible_caret_image) ImageView caretImage; 32 | 33 | CollapsibleParentModel model; 34 | 35 | CollapsibleParentViewHolder(View itemView) { 36 | super(itemView); 37 | ButterKnife.bind(this, itemView); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/collapsible/OnCollapsibleParentTap.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.collapsible; 17 | 18 | import com.groupon.android.featureadapter.sample.state.SampleModel; 19 | import com.groupon.featureadapter.events.FeatureEvent; 20 | import com.groupon.grox.commands.rxjava2.SingleActionCommand; 21 | 22 | class OnCollapsibleParentTap extends SingleActionCommand implements FeatureEvent { 23 | 24 | @Override 25 | public SampleModel newState(SampleModel oldState) { 26 | final boolean newIsCollapsed = !oldState.collapsibleFeatureState().isCollapsed; 27 | final CollapsibleFeatureState newCollapsibleFeatureState = new CollapsibleFeatureState(newIsCollapsed); 28 | return oldState 29 | .toBuilder() 30 | .setCollapsibleFeatureState(newCollapsibleFeatureState) 31 | .build(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/header/HeaderController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.header; 17 | 18 | import com.groupon.android.featureadapter.sample.model.Deal; 19 | import com.groupon.android.featureadapter.sample.model.Option; 20 | import com.groupon.android.featureadapter.sample.state.SampleModel; 21 | import com.groupon.featureadapter.AdapterViewTypeDelegate; 22 | import com.groupon.featureadapter.FeatureController; 23 | import com.groupon.featureadapter.ViewItem; 24 | 25 | import java.util.Collection; 26 | import java.util.List; 27 | 28 | import static java.util.Arrays.asList; 29 | import static java.util.Collections.emptyList; 30 | 31 | public class HeaderController extends FeatureController { 32 | 33 | private final ImageAdapterViewTypeDelegate imageDelegate = new ImageAdapterViewTypeDelegate(); 34 | private final TitleAdapterViewTypeDelegate titleDelegate = new TitleAdapterViewTypeDelegate(); 35 | 36 | @Override 37 | public Collection getAdapterViewTypeDelegates() { 38 | return asList(imageDelegate, titleDelegate); 39 | } 40 | 41 | @Override 42 | public List buildItems(SampleModel sampleModel) { 43 | Deal deal = sampleModel.deal(); 44 | if (deal == null) { 45 | return emptyList(); 46 | } 47 | return asList( 48 | new ViewItem<>(resolveImageUrl(sampleModel.selectedOption(), deal), imageDelegate), 49 | new ViewItem<>(resolveTitle(sampleModel.selectedOption(), deal), titleDelegate) 50 | ); 51 | } 52 | 53 | private Integer resolveImageUrl(Option selectedOption, Deal deal) { 54 | return selectedOption != null ? selectedOption.imageId : deal.imageId; 55 | } 56 | 57 | private String resolveTitle(Option selectedOption, Deal deal) { 58 | return selectedOption != null ? selectedOption.title : deal.title; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/header/ImageAdapterViewTypeDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.header; 17 | 18 | import androidx.recyclerview.widget.RecyclerView; 19 | import android.view.LayoutInflater; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.widget.ImageView; 23 | 24 | import com.groupon.android.featureadapter.sample.rx.R; 25 | import com.groupon.featureadapter.AdapterViewTypeDelegate; 26 | 27 | import butterknife.BindView; 28 | import butterknife.ButterKnife; 29 | 30 | class ImageAdapterViewTypeDelegate extends AdapterViewTypeDelegate { 31 | 32 | private static final int LAYOUT = R.layout.sample_header_image; 33 | 34 | @Override 35 | public ImageViewHolder createViewHolder(ViewGroup viewGroup) { 36 | return new ImageViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(LAYOUT, viewGroup, false)); 37 | } 38 | 39 | @Override 40 | public void bindViewHolder(ImageViewHolder holder, Integer imageId) { 41 | holder.headerImage.setImageResource(imageId); 42 | } 43 | 44 | @Override 45 | public void unbindViewHolder(ImageViewHolder holder) { 46 | // do nothing 47 | } 48 | 49 | static class ImageViewHolder extends RecyclerView.ViewHolder { 50 | 51 | @BindView(R.id.header_image) ImageView headerImage; 52 | 53 | ImageViewHolder(View itemView) { 54 | super(itemView); 55 | ButterKnife.bind(this, itemView); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/header/TitleAdapterViewTypeDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.header; 17 | 18 | import androidx.recyclerview.widget.RecyclerView; 19 | import android.view.LayoutInflater; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.widget.TextView; 23 | 24 | import com.groupon.android.featureadapter.sample.rx.R; 25 | import com.groupon.featureadapter.AdapterViewTypeDelegate; 26 | 27 | import butterknife.BindView; 28 | import butterknife.ButterKnife; 29 | 30 | class TitleAdapterViewTypeDelegate extends AdapterViewTypeDelegate { 31 | 32 | private static final int LAYOUT = R.layout.sample_header_title; 33 | 34 | @Override 35 | public TitleViewHolder createViewHolder(ViewGroup viewGroup) { 36 | return new TitleViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(LAYOUT, viewGroup, false)); 37 | } 38 | 39 | @Override 40 | public void bindViewHolder(TitleViewHolder holder, String s) { 41 | holder.titleText.setText(s); 42 | } 43 | 44 | @Override 45 | public void unbindViewHolder(TitleViewHolder holder) { 46 | // no op 47 | } 48 | 49 | static class TitleViewHolder extends RecyclerView.ViewHolder { 50 | 51 | @BindView(R.id.header_title_text) TextView titleText; 52 | 53 | TitleViewHolder(View itemView) { 54 | super(itemView); 55 | ButterKnife.bind(this, itemView); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/options/OnOptionClickEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.options; 17 | 18 | import com.groupon.android.featureadapter.sample.model.Deal; 19 | import com.groupon.android.featureadapter.sample.model.Option; 20 | import com.groupon.android.featureadapter.sample.state.SampleModel; 21 | import com.groupon.featureadapter.events.FeatureEvent; 22 | import com.groupon.grox.commands.rxjava2.Command; 23 | import com.groupon.grox.commands.rxjava2.SingleActionCommand; 24 | 25 | class OnOptionClickEvent extends SingleActionCommand implements FeatureEvent { 26 | 27 | private final String uuid; 28 | 29 | OnOptionClickEvent(String uuid) { 30 | this.uuid = uuid; 31 | } 32 | 33 | @Override 34 | public SampleModel newState(SampleModel model) { 35 | Option newOption = findOption(model.deal(), uuid); 36 | return model.toBuilder() 37 | .setSelectedOption(newOption != model.selectedOption() ? newOption : null) 38 | .build(); 39 | } 40 | 41 | private static Option findOption(Deal deal, String uuid) { 42 | for (Option option : deal.options) { 43 | if (uuid.equals(option.uuid)) return option; 44 | } 45 | throw new IllegalArgumentException("Option does not exist"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/options/OptionsAdapterViewTypeDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.options; 17 | 18 | import androidx.recyclerview.widget.RecyclerView; 19 | import android.view.LayoutInflater; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.widget.TextView; 23 | 24 | import com.groupon.android.featureadapter.sample.rx.R; 25 | import com.groupon.featureadapter.AdapterViewTypeDelegate; 26 | 27 | import butterknife.BindView; 28 | import butterknife.ButterKnife; 29 | 30 | class OptionsAdapterViewTypeDelegate extends AdapterViewTypeDelegate { 31 | 32 | private static final int LAYOUT = R.layout.sample_option; 33 | 34 | @Override 35 | public OptionsViewHolder createViewHolder(ViewGroup viewGroup) { 36 | return new OptionsViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(LAYOUT, viewGroup, false)); 37 | } 38 | 39 | @Override 40 | public void bindViewHolder(OptionsViewHolder holder, OptionsModel optionsModel) { 41 | holder.titleText.setText(optionsModel.title()); 42 | holder.titleText.setAllCaps(optionsModel.selected()); 43 | holder.priceText.setText(optionsModel.price()); 44 | holder.itemView.setOnClickListener(ignored -> fireEvent(new OnOptionClickEvent(optionsModel.uuid()))); 45 | } 46 | 47 | @Override 48 | public void unbindViewHolder(OptionsViewHolder holder) { 49 | // no op 50 | } 51 | 52 | static class OptionsViewHolder extends RecyclerView.ViewHolder { 53 | 54 | @BindView(R.id.option_title_text) TextView titleText; 55 | @BindView(R.id.option_price_text) TextView priceText; 56 | 57 | OptionsViewHolder(View itemView) { 58 | super(itemView); 59 | ButterKnife.bind(this, itemView); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/options/OptionsController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.options; 17 | 18 | import android.app.Activity; 19 | 20 | import com.groupon.android.featureadapter.sample.model.Deal; 21 | import com.groupon.android.featureadapter.sample.model.Option; 22 | import com.groupon.android.featureadapter.sample.state.SampleModel; 23 | import com.groupon.featureadapter.AdapterViewTypeDelegate; 24 | import com.groupon.featureadapter.FeatureAdapterItemDecoration; 25 | import com.groupon.featureadapter.FeatureController; 26 | import com.groupon.featureadapter.ViewItem; 27 | 28 | import java.util.ArrayList; 29 | import java.util.Collection; 30 | import java.util.List; 31 | 32 | import javax.inject.Inject; 33 | 34 | import static java.util.Collections.emptyList; 35 | import static java.util.Collections.singletonList; 36 | 37 | public class OptionsController extends FeatureController { 38 | 39 | private final OptionsAdapterViewTypeDelegate optionsDelegate = new OptionsAdapterViewTypeDelegate(); 40 | 41 | @Inject Activity activity; 42 | @Inject FeatureAdapterItemDecoration featureAdapterItemDecoration; 43 | 44 | private OptionsItemDecoration decoration; 45 | 46 | @Override 47 | public Collection getAdapterViewTypeDelegates() { 48 | return singletonList(optionsDelegate); 49 | } 50 | 51 | @Override 52 | public List buildItems(SampleModel sampleModel) { 53 | Deal deal = sampleModel.deal(); 54 | if (deal == null) { 55 | return emptyList(); 56 | } 57 | 58 | if (decoration == null) { 59 | decoration = new OptionsItemDecoration(activity); 60 | featureAdapterItemDecoration.registerFeatureDecoration(decoration, optionsDelegate); 61 | } 62 | 63 | List items = new ArrayList<>(deal.options.size()); 64 | for (Option option : deal.options) { 65 | items.add(new ViewItem<>(fromOption(option, sampleModel.selectedOption()), optionsDelegate)); 66 | } 67 | return items; 68 | } 69 | 70 | private static OptionsModel fromOption(Option option, Option selectedOption) { 71 | return OptionsModel.builder() 72 | .setUuid(option.uuid) 73 | .setTitle(option.title) 74 | .setPrice(option.price) 75 | .setSelected(option == selectedOption) 76 | .build(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/options/OptionsItemDecoration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.options; 17 | 18 | import android.content.Context; 19 | import android.graphics.Canvas; 20 | import android.graphics.Rect; 21 | import android.graphics.drawable.Drawable; 22 | import androidx.core.content.ContextCompat; 23 | import androidx.recyclerview.widget.RecyclerView; 24 | import android.view.View; 25 | 26 | import com.groupon.android.featureadapter.sample.rx.R; 27 | import com.groupon.featureadapter.FeatureItemDecoration; 28 | 29 | class OptionsItemDecoration implements FeatureItemDecoration { 30 | 31 | private final Drawable divider; 32 | private final Rect tempBounds = new Rect(); 33 | 34 | OptionsItemDecoration(Context context) { 35 | this.divider = ContextCompat.getDrawable(context, R.drawable.divider); 36 | } 37 | 38 | @Override 39 | public void getItemOffsetsImpl(Rect outRect, View view, RecyclerView.ViewHolder holder, RecyclerView parent, RecyclerView.State state) { 40 | outRect.bottom = divider.getIntrinsicHeight(); 41 | } 42 | 43 | @Override 44 | public void onDrawViewImpl(Canvas canvas, View view, RecyclerView.ViewHolder holder, RecyclerView parent, RecyclerView.State state) { 45 | canvas.save(); 46 | parent.getDecoratedBoundsWithMargins(view, tempBounds); 47 | divider.setBounds(tempBounds.left, tempBounds.bottom - divider.getIntrinsicHeight(), tempBounds.right, tempBounds.bottom); 48 | divider.draw(canvas); 49 | canvas.restore(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/features/options/OptionsModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.features.options; 17 | 18 | import com.google.auto.value.AutoValue; 19 | 20 | /** 21 | * It is not mandatory to use AutoValue to create the feature (small) models, we recommend it 22 | * as it generates valid equals and hashcode methods, and enforces immutable models. 23 | * (which are required) 24 | */ 25 | @AutoValue 26 | abstract class OptionsModel { 27 | abstract String uuid(); 28 | abstract String title(); 29 | abstract String price(); 30 | abstract boolean selected(); 31 | 32 | abstract Builder toBuilder(); 33 | 34 | static Builder builder() { 35 | return new AutoValue_OptionsModel.Builder(); 36 | } 37 | 38 | @AutoValue.Builder 39 | static abstract class Builder { 40 | abstract Builder setUuid(String uuid); 41 | abstract Builder setTitle(String title); 42 | abstract Builder setPrice(String price); 43 | abstract Builder setSelected(boolean selected); 44 | abstract OptionsModel build(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /feature-adapter-sample-rx/src/main/java/com/groupon/android/featureadapter/sample/model/Deal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017, Groupon, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.groupon.android.featureadapter.sample.model; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | public class Deal { 22 | public String title; 23 | public List