├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── saulmm │ │ └── avengers │ │ ├── TestData.java │ │ └── tests │ │ └── CharacterListActivityInstrumentationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── Abel.ttf │ ├── java │ │ └── saulmm │ │ │ └── avengers │ │ │ ├── AvengersApplication.java │ │ │ ├── ButterKnifeUtils.java │ │ │ ├── ResizeBehavior.java │ │ │ ├── injector │ │ │ ├── Activity.java │ │ │ ├── components │ │ │ │ ├── ActivityComponent.java │ │ │ │ ├── AppComponent.java │ │ │ │ ├── AvengerInformationComponent.java │ │ │ │ └── AvengersComponent.java │ │ │ └── modules │ │ │ │ ├── ActivityModule.java │ │ │ │ ├── AppModule.java │ │ │ │ └── AvengerInformationModule.java │ │ │ ├── mvp │ │ │ ├── presenters │ │ │ │ ├── CharacterDetailPresenter.java │ │ │ │ ├── CharacterListPresenter.java │ │ │ │ ├── CollectionPresenter.java │ │ │ │ └── Presenter.java │ │ │ └── views │ │ │ │ ├── CharacterDetailView.java │ │ │ │ ├── CharacterListView.java │ │ │ │ ├── CollectionView.java │ │ │ │ └── View.java │ │ │ ├── utils │ │ │ ├── DatabindingUtils.java │ │ │ ├── OnCharacterImageCallback.java │ │ │ ├── TransitionUtils.java │ │ │ └── Utils.java │ │ │ └── views │ │ │ ├── RecyclerClickListener.java │ │ │ ├── activities │ │ │ ├── CharacterDetailActivity.java │ │ │ ├── CharacterListActivity.java │ │ │ └── CollectionActivity.java │ │ │ ├── adapter │ │ │ └── AvengersListAdapter.java │ │ │ ├── utils │ │ │ ├── AnimUtils.java │ │ │ └── TransitionListenerAdapter.java │ │ │ └── views │ │ │ ├── CustomTextView.java │ │ │ ├── RecyclerInsetsDecoration.java │ │ │ └── SimpleDivider.java │ └── res │ │ ├── drawable-hdpi │ │ └── ic_action_favorite.png │ │ ├── drawable-mdpi │ │ └── ic_action_favorite.png │ │ ├── drawable-nodpi │ │ ├── avengers_header.png │ │ ├── error_placeholder.png │ │ ├── header_background.png │ │ └── marvel.png │ │ ├── drawable-xhdpi │ │ └── ic_action_favorite.png │ │ ├── drawable-xxhdpi │ │ └── ic_action_favorite.png │ │ ├── drawable-xxxhdpi │ │ └── ic_action_favorite.png │ │ ├── drawable │ │ ├── divider.xml │ │ ├── gradient_dark.xml │ │ ├── gradient_dark_up.xml │ │ └── ic_more.xml │ │ ├── layout │ │ ├── activity_avenger_detail.xml │ │ ├── activity_avengers_list.xml │ │ ├── activity_character_collection.xml │ │ ├── item_character.xml │ │ ├── item_comic.xml │ │ ├── view_error.xml │ │ ├── view_filter_dialog.xml │ │ └── view_loading_characters.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── integer.xml │ │ ├── string-arrays.xml │ │ ├── strings.xml │ │ ├── strings_character_list.xml │ │ ├── strings_common.xml │ │ ├── styles.xml │ │ └── themes.xml │ └── test │ └── java │ ├── DetailPresenterTest.java │ └── ListPresenterTest.java ├── art ├── cap.png ├── screen_detail.png └── screen_main.png ├── build.gradle ├── domain ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── saulmm │ │ └── avengers │ │ ├── CharacterDetailsUsecase.java │ │ ├── GetCharactersUsecase.java │ │ ├── GetCollectionUsecase.java │ │ └── Usecase.java │ └── test │ └── java │ ├── GetCharacterDetailsTest.java │ └── GetCharactersUsecaseTest.java ├── gradle.properties ├── model ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── saulmm │ │ └── avengers │ │ ├── entities │ │ ├── CollectionItem.java │ │ ├── Comic.java │ │ ├── ComicDate.java │ │ ├── ComicsCollection.java │ │ ├── ItemPreview.java │ │ ├── MarvelCharacter.java │ │ ├── TextObject.java │ │ └── Thumbnail.java │ │ ├── repository │ │ ├── CharacterRepository.java │ │ └── wrappers │ │ │ ├── ComicsWrapper.java │ │ │ ├── MarvelApiWrapper.java │ │ │ └── MarvelDataWrapper.java │ │ └── rest │ │ ├── Endpoint.java │ │ ├── HttpErrors.java │ │ ├── MarvelApi.java │ │ ├── MarvelAuthorizer.java │ │ ├── RestDataSource.java │ │ ├── exceptions │ │ ├── NetworkErrorException.java │ │ ├── NetworkTimeOutException.java │ │ ├── NetworkUknownHostException.java │ │ ├── ServerErrorException.java │ │ └── UknownErrorException.java │ │ └── utils │ │ ├── MarvelApiUtils.java │ │ ├── deserializers │ │ ├── MarvelResultsComicsDeserialiser.java │ │ └── MarvelResultsDeserializer.java │ │ └── interceptors │ │ └── MarvelSigningIterceptor.java │ └── test │ └── java │ └── GsonDeserializersTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | #Android generated 2 | bin 3 | gen 4 | lint.xml 5 | lint 6 | 7 | #Eclipse 8 | .project 9 | .classpath 10 | .settings 11 | .checkstyle 12 | 13 | #IntelliJ IDEA 14 | .idea 15 | *.iml 16 | *.ipr 17 | *.iws 18 | classes 19 | gen-external-apklibs 20 | 21 | #gradle 22 | .gradle 23 | local.properties 24 | gradlew 25 | gradlew.bat 26 | gradle/ 27 | build/ 28 | 29 | #vi 30 | *.swp 31 | 32 | #other editors 33 | *.bak 34 | 35 | #Maven 36 | target 37 | release.properties 38 | pom.xml.* 39 | 40 | #Ant 41 | build.xml 42 | ant.properties 43 | local.properties 44 | proguard.cfg 45 | proguard-project.txt 46 | 47 | #Other 48 | .DS_Store 49 | Thumbs.db 50 | tmp 51 | *.tgz 52 | *.lock 53 | *.lck 54 | com_crashlytics_export_strings.xml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License 2 | About the License 3 | 4 | Mozilla is the custodian of the Mozilla Public License ("MPL"), an open source/free software license. 5 | 6 | The current version of the license is MPL 2.0 (html | plain text). If you want to use or distribute code licensed under the MPL 2.0 and have questions about it, you may want to read the FAQ. 7 | 8 | MPL 2.0 Revision Process 9 | 10 | The release of MPL 2.0 was the result of a two year process that revised MPL 1.1. A Revision FAQ documents this process, and explains the most significant changes made. 11 | 12 | Historical Documents 13 | 14 | Various historical documents relating to the Mozilla and Netscape Public Licenses are available, including deprecated versions of the license such as MPL 1.1. 15 | 16 | Mozilla Licensing Information 17 | 18 | The Mozilla Project is only one of many users of the MPL, but because many people come to this page looking for information about Mozilla's open source licensing policies and practices, we've provided the information below as a reference. 19 | 20 | Correctly Licensing New Source Code 21 | 22 | Any new code checked into Mozilla's source repositories needs to comply with Mozilla's source code licensing policy. Please use the appropriate header text at the top of each file. 23 | 24 | Licenses For Existing Source Code 25 | 26 | Most Mozilla software projects use the MPL, but some have different terms. Detailed information on the licensing of existing code can be found by inspecting its license headers, or by visiting the license information page in the relevant Mozilla software. 27 | 28 | For information on how other things are licensed, including Mozilla's trademarks and websites, see our general licensing information page. 29 | 30 | Questions? 31 | 32 | If, after reading all the above carefully (particularly the FAQ) you have a further question about the MPL or the licensing terms of Mozilla project code, please send it to licensing@mozilla.org. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### When Avengers meet Dagger2, RxJava & Retrofit in a clean way 3 | 4 | This is the source code of a series focused on giving some basic ideas about how to use [Retrofit](http://square.github.io/retrofit/), [Dagger2](http://google.github.io/dagger/) & [RxJava](https://github.com/ReactiveX/RxJava) together with a [Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html). 5 | 6 | ##### [Part 1 - Dagger 2](http://saulmm.github.io/when-Thor-and-Hulk-meet-dagger2-rxjava-1) 7 | 8 | In this first part it explains how Dagger 2 can help the decoupling of the layers in a project, removing dependencies so that it is easily scalable and testable. 9 | 10 | ##### [Part 2 - RxJava, RxAndroid, Reactive Extensions & operators](http://saulmm.github.io/when-Iron-Man-becomes-Reactive-Avengers2) 11 | 12 | This part focuses on the understanding of what are the Reactive Extensions, its Java implementation, and use RxJava operators, all it integrated with a clean architecture 13 | 14 | *NOTE*: The source shown in articles refers to [this release]. 15 | 16 | #### Screenshots 17 | 18 | ![](./art/screen_detail.png) 19 | 20 | ### License 21 | 22 | ``` 23 | This Source Code Form is subject to the terms of the Mozilla Public 24 | License, v. 2.0. If a copy of the MPL was not distributed with this 25 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 26 | ``` 27 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'me.tatarka.retrolambda' 2 | apply plugin: 'com.android.application' 3 | apply plugin: 'com.neenbedankt.android-apt' 4 | apply plugin: 'com.android.databinding' 5 | 6 | def cfg = rootProject.ext.configuration 7 | def libs = rootProject.ext.libraries; 8 | def test = rootProject.ext.testingLibraries; 9 | 10 | android { 11 | compileSdkVersion cfg.compileVersion 12 | buildToolsVersion cfg.buildToolsVersion 13 | 14 | //noinspection GroovyAssignabilityCheck 15 | defaultConfig { 16 | applicationId cfg.package 17 | minSdkVersion cfg.compileVersion 18 | targetSdkVersion cfg.targetSdk 19 | versionCode cfg.version_code 20 | versionName cfg.version_name 21 | 22 | buildConfigField "String", "MARVEL_PUBLIC_KEY", "\"${marvel_public_key}\"" 23 | buildConfigField "String", "MARVEL_PRIVATE_KEY", "\"${marvel_private_key}\"" 24 | 25 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | //noinspection GroovyAssignabilityCheck 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | 34 | buildToolsVersion '23.0.3' 35 | } 36 | 37 | //noinspection GroovyAssignabilityCheck 38 | dependencies { 39 | compile project(':domain') 40 | 41 | compile "com.github.bumptech.glide:glide:${libs.glide}" 42 | compile "jp.wasabeef:recyclerview-animators:${libs.recycler_animators}" 43 | compile "com.jakewharton:butterknife:${libs.butterknife}" 44 | compile "de.hdodenhof:circleimageview:${libs.circleimageview}" 45 | compile "io.reactivex:rxandroid:${libs.rxandroid}" 46 | 47 | compile "com.squareup.retrofit:retrofit:${libs.retrofit}" 48 | compile "com.squareup.retrofit:converter-gson:${libs.retrofit}" 49 | compile "com.squareup.retrofit:adapter-rxjava:${libs.retrofit}" 50 | 51 | apt "com.google.dagger:dagger-compiler:${libs.dagger}" 52 | 53 | // Android 54 | compile "com.android.support:design:${libs.supportVersion}" 55 | compile "com.android.support:appcompat-v7:${libs.supportVersion}" 56 | compile "com.android.support:cardview-v7:${libs.supportVersion}" 57 | compile "com.android.support:recyclerview-v7:${libs.supportVersion}" 58 | compile "com.android.support:palette-v7:${libs.supportVersion}" 59 | 60 | // Test 61 | testCompile "org.mockito:mockito-core:$test.mockito" 62 | testCompile "junit:junit:$test.junit" 63 | 64 | androidTestCompile "com.squareup.okhttp:mockwebserver:$libs.okhttp" 65 | androidTestCompile "com.android.support.test:runner:$test.espressoRunner" 66 | androidTestCompile "com.android.support.test:rules:$test.espressoRules" 67 | androidTestCompile("com.android.support.test.espresso:espresso-core:$test.espresso") { 68 | exclude group: 'javax.inject' 69 | } 70 | 71 | androidTestCompile "com.android.support.test.espresso:espresso-intents:$test.espresso" 72 | androidTestCompile ("com.android.support.test.espresso:espresso-contrib:$test.espresso") { 73 | exclude group: 'com.android.support', module: 'appcompat' 74 | exclude group: 'com.android.support', module: 'support-v4' 75 | exclude module: 'recyclerview-v7' 76 | } 77 | 78 | androidTestApt "com.google.dagger:dagger-compiler:${libs.dagger}" 79 | 80 | } 81 | 82 | configurations.all { 83 | resolutionStrategy.force "com.android.support:support-annotations:$libs.supportVersion" 84 | resolutionStrategy.force "com.squareup.okhttp:okhttp:$libs.okhttp" 85 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/saulmm/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 | -------------------------------------------------------------------------------- /app/src/androidTest/java/saulmm/avengers/TestData.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers; 2 | 3 | /** 4 | * Created by saulmm on 04/01/16. 5 | */ 6 | public final class TestData { 7 | 8 | public static final String SINGLE_CHARACTER_JSON = "{\"code\":200,\"status\":\"Ok\",\"copyright\":\"© 2016 MARVEL\",\"attributionText\":\"Data provided by Marvel. © 2016 MARVEL\",\"attributionHTML\":\"Data provided by Marvel. © 2016 MARVEL\",\"etag\":\"352196c55578b344079a81ffea21343cc8843d38\",\"data\":{\"offset\":0,\"limit\":20,\"total\":1,\"count\":1,\"results\":[{\"id\":1011170,\"name\":\"Alain\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1011170\",\"comics\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011170/comics\",\"items\":[],\"returned\":0},\"series\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011170/series\",\"items\":[],\"returned\":0},\"stories\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011170/stories\",\"items\":[],\"returned\":0},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011170/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/116/alain?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Alain?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1011170/alain?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]}]}}"; 9 | 10 | 11 | public static final String TWENTY_CHARACTERS_JSON = "{\"code\":200,\"status\":\"Ok\",\"copyright\":\"© 2016 MARVEL\",\"attributionText\":\"Data provided by Marvel. © 2016 MARVEL\",\"attributionHTML\":\"Data provided by Marvel. © 2016 MARVEL\",\"etag\":\"46495bc6b3f0e9f7348c3be2715f9cf27d3daeb1\",\"data\":{\"offset\":60,\"limit\":20,\"total\":1485,\"count\":20,\"results\":[{\"id\":1010686,\"name\":\"Arcana\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1010686\",\"comics\":{\"available\":2,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010686/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/3908\",\"name\":\"Squadron Supreme (2006) #1\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/5425\",\"name\":\"Squadron Supreme Vol. 1: The Pre-War Years Premiere (Hardcover)\"}],\"returned\":2},\"series\":{\"available\":2,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010686/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/944\",\"name\":\"Squadron Supreme (2006)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/1791\",\"name\":\"Squadron Supreme Vol. 1: The Pre-War Years Premiere (2006)\"}],\"returned\":2},\"stories\":{\"available\":1,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010686/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/5249\",\"name\":\"1 of 6 - The Pre-War Years\",\"type\":\"cover\"}],\"returned\":1},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010686/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/179/arcana?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1010686/arcana?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1009159,\"name\":\"Archangel\",\"description\":\"\",\"modified\":\"2013-10-18T12:48:24-0400\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/8/03/526165ed93180\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1009159\",\"comics\":{\"available\":531,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009159/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/17701\",\"name\":\"Age of Apocalypse: The Chosen (1995) #1\"}],\"returned\":1},\"series\":{\"available\":137,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009159/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/3614\",\"name\":\"Age of Apocalypse: The Chosen (1995)\"}],\"returned\":1},\"stories\":{\"available\":576,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009159/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/3261\",\"name\":\"2 of 2 - Save the Life of My Child\",\"type\":\"cover\"}],\"returned\":1},\"events\":{\"available\":17,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009159/events\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/116\",\"name\":\"Acts of Vengeance!\"}],\"returned\":1},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/1/angel?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Angel_(Warren_Worthington_III)?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1009159/archangel?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1009160,\"name\":\"Arclight\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/5/f0/4c0042067fd8b\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1009160\",\"comics\":{\"available\":13,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009160/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/26024\",\"name\":\"New Avengers Annual (2009) #3\"}],\"returned\":1},\"series\":{\"available\":5,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009160/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/8140\",\"name\":\"New Avengers Annual (2009)\"}],\"returned\":1},\"stories\":{\"available\":12,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009160/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/22086\",\"name\":\"Fallen Angel!\",\"type\":\"interiorStory\"}],\"returned\":1},\"events\":{\"available\":3,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009160/events\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/252\",\"name\":\"Inferno\"}],\"returned\":1},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/182/arclight?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Arclight?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1009160/arclight?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1010784,\"name\":\"Ares\",\"description\":\"\",\"modified\":\"2014-04-29T14:50:59-0400\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/c/10/535ff3daea603\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1010784\",\"comics\":{\"available\":41,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010784/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/24348\",\"name\":\"Adam: Legend of the Blue Marvel (Trade Paperback)\"}],\"returned\":1},\"series\":{\"available\":17,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010784/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/7524\",\"name\":\"Adam: Legend of the Blue Marvel (2008)\"}],\"returned\":1},\"stories\":{\"available\":43,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010784/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/5532\",\"name\":\"1 of 5 xLS\",\"type\":\"cover\"}],\"returned\":1},\"events\":{\"available\":4,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010784/events\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/296\",\"name\":\"Chaos War\"}],\"returned\":1},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/183/ares?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Ares?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1010784/ares?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1011275,\"name\":\"Argent\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1011275\",\"comics\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011275/comics\",\"items\":[],\"returned\":0},\"series\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011275/series\",\"items\":[],\"returned\":0},\"stories\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011275/stories\",\"items\":[],\"returned\":0},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011275/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/184/argent?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Argent?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1011275/argent?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1011012,\"name\":\"Armadillo\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/2/40/4c0032754da02\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1011012\",\"comics\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011012/comics\",\"items\":[],\"returned\":0},\"series\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011012/series\",\"items\":[],\"returned\":0},\"stories\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011012/stories\",\"items\":[],\"returned\":0},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011012/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/189/armadillo?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Armadillo?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1011012/armadillo?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1011298,\"name\":\"Armor (Hiscanao Ichiki)\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/9/20/4c002e6cbf990\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1011298\",\"comics\":{\"available\":4,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011298/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/21511\",\"name\":\"Astonishing X-Men (2004) #25\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/38320\",\"name\":\"Astonishing X-Men (2004) #37\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/38318\",\"name\":\"Astonishing X-Men (2004) #38\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/41548\",\"name\":\"Wolverine & the X-Men: Alpha & Omega (2011) #3\"}],\"returned\":4},\"series\":{\"available\":2,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011298/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/744\",\"name\":\"Astonishing X-Men (2004 - 2013)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/15486\",\"name\":\"Wolverine & the X-Men: Alpha & Omega (2011 - 2012)\"}],\"returned\":2},\"stories\":{\"available\":4,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011298/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/47427\",\"name\":\"1 of 6 Ghost Box\",\"type\":\"cover\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/84191\",\"name\":\"Cover #84191\",\"type\":\"cover\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/89900\",\"name\":\"Astonishing X-Men (2004) #38\",\"type\":\"interiorStory\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/93985\",\"name\":\"Cover #93985\",\"type\":\"cover\"}],\"returned\":4},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011298/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/191/armor?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Armor_(Hisako_Ichiki)?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1011298/armor_hisako_ichiki?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1010827,\"name\":\"Armory\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1010827\",\"comics\":{\"available\":2,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010827/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/17293\",\"name\":\"Avengers: The Initiative Annual (2007) #1\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/21085\",\"name\":\"Secret Invasion: The Infiltration (Trade Paperback)\"}],\"returned\":2},\"series\":{\"available\":2,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010827/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/3087\",\"name\":\"Avengers: The Initiative Annual (2007)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/4619\",\"name\":\"Secret Invasion: The Infiltration (2008)\"}],\"returned\":2},\"stories\":{\"available\":1,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010827/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/36242\",\"name\":\"1 of 1\",\"type\":\"cover\"}],\"returned\":1},\"events\":{\"available\":1,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010827/events\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/269\",\"name\":\"Secret Invasion\"}],\"returned\":1},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/193/armory?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1010827/armory?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1009740,\"name\":\"Arnim Zola\",\"description\":\"The frail, dwarfish Arnim Zola was born in 1930s Switzerland where he became the world's leading biochemist and genetic engineer.\",\"modified\":\"2012-03-20T12:33:28-0400\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/8/b0/4c00393a4cb7c\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1009740\",\"comics\":{\"available\":6,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009740/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/5402\",\"name\":\"Captain America (2004) #24\"}],\"returned\":1},\"series\":{\"available\":5,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009740/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/1996\",\"name\":\"Captain America (1968 - 1996)\"}],\"returned\":1},\"stories\":{\"available\":5,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009740/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/4276\",\"name\":\"3 of 3 - Civil War\",\"type\":\"interiorStory\"}],\"returned\":1},\"events\":{\"available\":1,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009740/events\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/238\",\"name\":\"Civil War\"}],\"returned\":1},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/2790/arnim_zola?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Zola,_Arnim?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1009740/arnim_zola?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1010748,\"name\":\"Arsenic\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/8/c0/4c00359a2be7b\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1010748\",\"comics\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010748/comics\",\"items\":[],\"returned\":0},\"series\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010748/series\",\"items\":[],\"returned\":0},\"stories\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010748/stories\",\"items\":[],\"returned\":0},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010748/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/197/arsenic?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Arsenic_(and_Old_Lace)?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1010748/arsenic?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1009161,\"name\":\"Artiee\",\"description\":\"\",\"modified\":\"2011-10-27T09:59:16-0400\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1009161\",\"comics\":{\"available\":1,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009161/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/35509\",\"name\":\"Amazing Spider-Man (1999) #673\"}],\"returned\":1},\"series\":{\"available\":1,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009161/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/454\",\"name\":\"Amazing Spider-Man (1999 - 2013)\"}],\"returned\":1},\"stories\":{\"available\":1,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009161/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/95451\",\"name\":\"Amazing Spider-Man (1999) #673\",\"type\":\"cover\"}],\"returned\":1},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009161/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/198/artiee?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1009161/artiee?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1010718,\"name\":\"Asgardian\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1010718\",\"comics\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010718/comics\",\"items\":[],\"returned\":0},\"series\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010718/series\",\"items\":[],\"returned\":0},\"stories\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010718/stories\",\"items\":[],\"returned\":0},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010718/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/201/asgardian?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Lorelei_%28Asgardian%29?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1010718/asgardian?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1009162,\"name\":\"Askew-Tronics\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1009162\",\"comics\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009162/comics\",\"items\":[],\"returned\":0},\"series\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009162/series\",\"items\":[],\"returned\":0},\"stories\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009162/stories\",\"items\":[],\"returned\":0},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009162/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/204/askew-tronics?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1009162/askew-tronics?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1010835,\"name\":\"Asylum\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1010835\",\"comics\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010835/comics\",\"items\":[],\"returned\":0},\"series\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010835/series\",\"items\":[],\"returned\":0},\"stories\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010835/stories\",\"items\":[],\"returned\":0},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010835/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/211/asylum?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Asylum_(Henrique_Gallante)?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1010835/asylum?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1010336,\"name\":\"Atlas (Team)\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1010336\",\"comics\":{\"available\":12,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010336/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/17493\",\"name\":\"Avengers (1998) #12\"}],\"returned\":1},\"series\":{\"available\":8,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010336/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/354\",\"name\":\"Avengers (1998 - 2004)\"}],\"returned\":1},\"stories\":{\"available\":8,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010336/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/3878\",\"name\":\"6 of 6 - One Step Forward\",\"type\":\"cover\"}],\"returned\":1},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1010336/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/214/atlas?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Atlas_(Team)?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1010336/atlas_team?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1009163,\"name\":\"Aurora\",\"description\":\"\",\"modified\":\"2011-05-10T15:56:51-0400\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/f/10/4c004203f1072\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1009163\",\"comics\":{\"available\":53,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009163/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/17701\",\"name\":\"Age of Apocalypse: The Chosen (1995) #1\"}],\"returned\":1},\"series\":{\"available\":14,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009163/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/3614\",\"name\":\"Age of Apocalypse: The Chosen (1995)\"}],\"returned\":1},\"stories\":{\"available\":59,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009163/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/15427\",\"name\":\"Shoot-Out at the Stampede!\",\"type\":\"interiorStory\"}],\"returned\":20},\"events\":{\"available\":4,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009163/events\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/227\",\"name\":\"Age of Apocalypse\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/296\",\"name\":\"Chaos War\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/302\",\"name\":\"Fear Itself\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/29\",\"name\":\"Infinity War\"}],\"returned\":4},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/221/aurora?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Aurora?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1009163/aurora?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1009164,\"name\":\"Avalanche\",\"description\":\"\",\"modified\":\"1969-12-31T19:00:00-0500\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/f/10/4c0042010d383\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1009164\",\"comics\":{\"available\":30,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009164/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/6929\",\"name\":\"Avengers Annual (1967) #15\"}],\"returned\":1},\"series\":{\"available\":9,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009164/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/1988\",\"name\":\"Avengers Annual (1967 - 1994)\"}],\"returned\":1},\"stories\":{\"available\":37,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009164/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/15472\",\"name\":\"Days of Future Past\",\"type\":\"interiorStory\"}],\"returned\":1},\"events\":{\"available\":5,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009164/events\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/246\",\"name\":\"Evolutionary War\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/248\",\"name\":\"Fall of the Mutants\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/32\",\"name\":\"Kings of Pain\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/263\",\"name\":\"Mutant Massacre\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/270\",\"name\":\"Secret Wars\"}],\"returned\":5},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/222/avalanche?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Avalanche?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1009164/avalanche?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1009165,\"name\":\"Avengers\",\"description\":\"Earth's Mightiest Heroes joined forces to take on threats that were too big for any one hero to tackle. With a roster that has included Captain America, Iron Man, Ant-Man, Hulk, Thor, Wasp and dozens more over the years, the Avengers have come to be regarded as Earth's No. 1 team.\",\"modified\":\"2014-05-27T20:28:26-0400\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/9/20/5102c774ebae7\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1009165\",\"comics\":{\"available\":962,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009165/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/35501\",\"name\":\"Amazing Spider-Man (1999) #663\"}],\"returned\":1},\"series\":{\"available\":191,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009165/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/15331\",\"name\":\"Age of Apocalypse (2011 - 2013)\"}],\"returned\":1},\"stories\":{\"available\":1373,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009165/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/490\",\"name\":\"Interior #490\",\"type\":\"interiorStory\"}],\"returned\":1},\"events\":{\"available\":21,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1009165/events\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/events/116\",\"name\":\"Acts of Vengeance!\"}],\"returned\":1},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/comics/characters/1009165/avengers?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Avengers?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1009165/avengers?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1015239,\"name\":\"Avengers (Ultimate)\",\"description\":\"\",\"modified\":\"2012-07-10T19:18:28-0400\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1015239\",\"comics\":{\"available\":23,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1015239/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/15718\",\"name\":\"Ultimate X-Men (2000) #27\"}],\"returned\":1},\"series\":{\"available\":9,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1015239/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/9867\",\"name\":\"Ultimate Comics Avengers 3 (2010 - 2011)\"}],\"returned\":1},\"stories\":{\"available\":16,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1015239/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/32194\",\"name\":\"Free Preview of AVENGERS 65\",\"type\":\"interiorStory\"}],\"returned\":1},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1015239/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/68/avengers?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1015239/avengers_ultimate?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]},{\"id\":1011766,\"name\":\"Azazel (Mutant)\",\"description\":\"A mutant from biblical times, Azazel is the ruler of the Neyaphem and claims that the Earth and everything on it belongs to him.\",\"modified\":\"2011-06-09T11:04:52-0400\",\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"resourceURI\":\"http://gateway.marvel.com/v1/public/characters/1011766\",\"comics\":{\"available\":2,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011766/comics\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/13969\",\"name\":\"Uncanny X-Men (1963) #428\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/41821\",\"name\":\"X-MEN FIRST CLASS: THE HIGH HAND (2011) #1\"}],\"returned\":2},\"series\":{\"available\":2,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011766/series\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/2258\",\"name\":\"Uncanny X-Men (1963 - 2011)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/15598\",\"name\":\"X-MEN FIRST CLASS: THE HIGH HAND (2011)\"}],\"returned\":2},\"stories\":{\"available\":2,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011766/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/28405\",\"name\":\"How Did I Get Here?\",\"type\":\"interiorStory\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/94626\",\"name\":\"X-MEN FIRST CLASS: THE HIGH HAND (2011) #1\",\"type\":\"interiorStory\"}],\"returned\":2},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/characters/1011766/events\",\"items\":[],\"returned\":0},\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/characters/227/azazel?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"wiki\",\"url\":\"http://marvel.com/universe/Azazel_(mutant)?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"},{\"type\":\"comiclink\",\"url\":\"http://marvel.com/comics/characters/1011766/azazel_mutant?utm_campaign=apiRef&utm_source=74129ef99c9fd5f7692608f17abb88f9\"}]}]}}"; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/androidTest/java/saulmm/avengers/tests/CharacterListActivityInstrumentationTest.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.tests; 2 | 3 | import android.content.Intent; 4 | import android.support.test.espresso.Espresso; 5 | import android.support.test.espresso.contrib.RecyclerViewActions; 6 | import android.support.test.espresso.intent.Intents; 7 | import android.support.test.espresso.intent.rule.IntentsTestRule; 8 | import android.support.test.espresso.matcher.ViewMatchers; 9 | import android.support.test.rule.ActivityTestRule; 10 | import android.support.test.runner.AndroidJUnit4; 11 | import android.test.suitebuilder.annotation.LargeTest; 12 | import android.util.Log; 13 | import android.widget.TextView; 14 | 15 | import com.squareup.okhttp.mockwebserver.Dispatcher; 16 | import com.squareup.okhttp.mockwebserver.MockResponse; 17 | import com.squareup.okhttp.mockwebserver.MockWebServer; 18 | import com.squareup.okhttp.mockwebserver.RecordedRequest; 19 | 20 | import org.junit.After; 21 | import org.junit.Before; 22 | import org.junit.Rule; 23 | import org.junit.Test; 24 | import org.junit.runner.RunWith; 25 | 26 | import javax.inject.Inject; 27 | 28 | import saulmm.avengers.R; 29 | import saulmm.avengers.TestData; 30 | import saulmm.avengers.injector.components.DaggerAvengersComponent; 31 | import saulmm.avengers.injector.modules.ActivityModule; 32 | import saulmm.avengers.repository.CharacterRepository; 33 | import saulmm.avengers.rest.RestDataSource; 34 | import saulmm.avengers.views.activities.CharacterDetailActivity; 35 | import saulmm.avengers.views.activities.CharacterListActivity; 36 | 37 | import static android.support.test.espresso.Espresso.*; 38 | import static android.support.test.espresso.action.ViewActions.click; 39 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 40 | import static android.support.test.espresso.intent.Intents.intended; 41 | import static android.support.test.espresso.intent.Intents.times; 42 | import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent; 43 | import static android.support.test.espresso.matcher.ViewMatchers.*; 44 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 45 | import static org.hamcrest.CoreMatchers.allOf; 46 | import static org.hamcrest.CoreMatchers.instanceOf; 47 | import static org.hamcrest.CoreMatchers.is; 48 | 49 | @LargeTest 50 | @RunWith(AndroidJUnit4.class) 51 | public class CharacterListActivityInstrumentationTest { 52 | private MockWebServer mMockWebServer; 53 | 54 | @Rule public ActivityTestRule mCharacterListIntentRule = 55 | new ActivityTestRule<>(CharacterListActivity.class, true, false); 56 | 57 | @Before public void setUp() throws Exception { 58 | mMockWebServer = new MockWebServer(); 59 | mMockWebServer.start(); 60 | 61 | RestDataSource.END_POINT = mMockWebServer.getUrl("/").toString(); 62 | } 63 | 64 | @Test public void showCharacters() { 65 | mMockWebServer.enqueue(new MockResponse().setBody(TestData.TWENTY_CHARACTERS_JSON)); 66 | 67 | mCharacterListIntentRule.launchActivity(null); 68 | 69 | onView(withId(R.id.activity_avengers_recycler)).check(matches(isDisplayed())); 70 | } 71 | 72 | @After public void tearDown() throws Exception { 73 | mMockWebServer.shutdown(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/assets/Abel.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saulmm/Avengers/2b02f19d5a0ca3e36b5370f545036b30fd62a114/app/src/main/assets/Abel.ttf -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/AvengersApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers; 7 | 8 | import android.app.Application; 9 | 10 | import saulmm.avengers.injector.modules.AppModule; 11 | import saulmm.avengers.injector.components.AppComponent; 12 | import saulmm.avengers.injector.components.DaggerAppComponent; 13 | 14 | public class AvengersApplication extends Application { 15 | private AppComponent mAppComponent; 16 | 17 | @Override 18 | public void onCreate() { 19 | super.onCreate(); 20 | initializeInjector(); 21 | } 22 | 23 | private void initializeInjector() { 24 | mAppComponent = DaggerAppComponent.builder() 25 | .appModule(new AppModule(this)) 26 | .build(); 27 | } 28 | 29 | public AppComponent getAppComponent() { 30 | return mAppComponent; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/ButterKnifeUtils.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers; 2 | 3 | import android.widget.TextView; 4 | import butterknife.ButterKnife; 5 | 6 | public class ButterKnifeUtils { 7 | 8 | public static final ButterKnife.Setter TEXTCOLOR = new ButterKnife.Setter() { 9 | 10 | @Override 11 | public void set(TextView view, Integer value, int index) { 12 | view.setTextColor(value); 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/ResizeBehavior.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers; 2 | 3 | import android.content.Context; 4 | import android.support.design.widget.AppBarLayout; 5 | import android.support.design.widget.CoordinatorLayout; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | import android.widget.ImageView; 9 | 10 | @SuppressWarnings("unused") 11 | public class ResizeBehavior extends CoordinatorLayout.Behavior { 12 | private final static String TAG = "behavior"; 13 | 14 | private float mFinalLeftAvatarPadding; 15 | private float mStartPosition; 16 | private float mStartToolbarPosition; 17 | private int mStartWidth; 18 | private int mFinalWidth; 19 | 20 | public ResizeBehavior(Context context, AttributeSet attrs) { 21 | } 22 | 23 | private int finalHeight; 24 | private int mStartHeight; 25 | 26 | @Override 27 | public boolean layoutDependsOn(CoordinatorLayout parent, ImageView child, View dependency) { 28 | return dependency instanceof AppBarLayout; 29 | } 30 | 31 | @Override 32 | public boolean onDependentViewChanged(CoordinatorLayout parent, ImageView child, View dependency) { 33 | shouldInitProperties(child, dependency); 34 | float expandedPercentageFactor = dependency.getY() / mStartHeight; 35 | 36 | // The child will remain always in the middle of its dependency 37 | child.setY(((dependency.getY() /2) + (dependency.getHeight() /2)) - child.getHeight()/2); 38 | float collapsedHeight = ((mStartHeight) * (1f - Math.abs(expandedPercentageFactor))); 39 | 40 | CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams(); 41 | if (collapsedHeight > finalHeight) lp.height = (int) collapsedHeight; 42 | child.setLayoutParams(lp); 43 | return true; 44 | } 45 | 46 | private void shouldInitProperties(ImageView child, View dependency) { 47 | if (mStartHeight == 0) 48 | mStartHeight = child.getHeight(); 49 | 50 | if (mStartWidth == 0) 51 | mStartWidth = child.getWidth(); 52 | 53 | if (finalHeight == 0) finalHeight = 54 | child.getContext().getResources().getDimensionPixelOffset(R.dimen.image_final_height); 55 | 56 | if (mStartToolbarPosition == 0) 57 | mStartToolbarPosition = dependency.getY() + (dependency.getHeight()/2); 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/injector/Activity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 android10.org. All rights reserved. 3 | * @author Fernando Cejas (the android10 coder) 4 | */ 5 | package saulmm.avengers.injector; 6 | 7 | import java.lang.annotation.Retention; 8 | 9 | import javax.inject.Scope; 10 | 11 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 12 | 13 | 14 | @Scope @Retention(RUNTIME) 15 | public @interface Activity {} 16 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/injector/components/ActivityComponent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.injector.components; 7 | 8 | 9 | import android.content.Context; 10 | import dagger.Component; 11 | import saulmm.avengers.injector.Activity; 12 | import saulmm.avengers.injector.modules.ActivityModule; 13 | 14 | @Activity 15 | @Component(dependencies = AppComponent.class, modules = ActivityModule.class) 16 | public interface ActivityComponent { 17 | 18 | Context context(); 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/injector/components/AppComponent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.injector.components; 7 | 8 | import dagger.Component; 9 | 10 | import javax.inject.Named; 11 | import javax.inject.Singleton; 12 | 13 | import rx.Scheduler; 14 | import saulmm.avengers.AvengersApplication; 15 | import saulmm.avengers.injector.modules.AppModule; 16 | import saulmm.avengers.repository.CharacterRepository; 17 | import saulmm.avengers.rest.Endpoint; 18 | import saulmm.avengers.rest.MarvelAuthorizer; 19 | 20 | @Singleton @Component(modules = AppModule.class) 21 | public interface AppComponent { 22 | AvengersApplication app(); 23 | CharacterRepository dataRepository(); 24 | Endpoint restEndpoint(); 25 | MarvelAuthorizer marvelAuthorizer(); 26 | 27 | @Named("ui_thread") Scheduler uiThread(); 28 | @Named("executor_thread") Scheduler executorThread(); 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/injector/components/AvengerInformationComponent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.injector.components; 7 | 8 | import dagger.Component; 9 | import saulmm.avengers.CharacterDetailsUsecase; 10 | import saulmm.avengers.GetCollectionUsecase; 11 | import saulmm.avengers.injector.Activity; 12 | import saulmm.avengers.injector.modules.ActivityModule; 13 | import saulmm.avengers.injector.modules.AvengerInformationModule; 14 | import saulmm.avengers.views.activities.CharacterDetailActivity; 15 | import saulmm.avengers.views.activities.CollectionActivity; 16 | 17 | @Activity 18 | @Component(dependencies = AppComponent.class, modules = {AvengerInformationModule.class, ActivityModule.class}) 19 | public interface AvengerInformationComponent extends ActivityComponent { 20 | 21 | void inject (CharacterDetailActivity detailActivity); 22 | 23 | void inject (CollectionActivity detailActivity); 24 | 25 | CharacterDetailsUsecase getCharacerInformationUsecase(); 26 | GetCollectionUsecase getCollectionUsecase(); 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/injector/components/AvengersComponent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.injector.components; 7 | 8 | import android.content.Context; 9 | 10 | import dagger.Component; 11 | import saulmm.avengers.injector.Activity; 12 | import saulmm.avengers.injector.modules.ActivityModule; 13 | import saulmm.avengers.views.activities.CharacterListActivity; 14 | 15 | @Activity 16 | @Component(dependencies = AppComponent.class, modules = {ActivityModule.class}) 17 | public interface AvengersComponent extends ActivityComponent { 18 | void inject (CharacterListActivity activity); 19 | 20 | Context activityContext(); 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/injector/modules/ActivityModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.injector.modules; 7 | 8 | 9 | import android.content.Context; 10 | 11 | import dagger.Module; 12 | import dagger.Provides; 13 | import javax.inject.Named; 14 | import rx.Scheduler; 15 | import rx.android.schedulers.AndroidSchedulers; 16 | import rx.schedulers.Schedulers; 17 | import saulmm.avengers.injector.Activity; 18 | 19 | @Module 20 | public class ActivityModule { 21 | private final Context mContext; 22 | 23 | public ActivityModule(Context mContext) { 24 | this.mContext = mContext; 25 | } 26 | 27 | @Provides @Activity 28 | Context provideActivityContext() { 29 | return mContext; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/injector/modules/AppModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.injector.modules; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | 11 | import javax.inject.Named; 12 | import javax.inject.Singleton; 13 | 14 | import rx.Scheduler; 15 | import rx.android.schedulers.AndroidSchedulers; 16 | import rx.schedulers.Schedulers; 17 | import saulmm.avengers.AvengersApplication; 18 | import saulmm.avengers.BuildConfig; 19 | import saulmm.avengers.repository.CharacterRepository; 20 | import saulmm.avengers.rest.Endpoint; 21 | import saulmm.avengers.rest.MarvelAuthorizer; 22 | import saulmm.avengers.rest.RestDataSource; 23 | 24 | @Module 25 | public class AppModule { 26 | private final AvengersApplication mAvengersApplication; 27 | 28 | public AppModule(AvengersApplication avengersApplication) { 29 | this.mAvengersApplication = avengersApplication; 30 | } 31 | 32 | @Provides @Singleton 33 | AvengersApplication provideAvengersApplicationContext() { 34 | return mAvengersApplication; } 35 | 36 | @Provides 37 | MarvelAuthorizer provideMarvelAuthorizer() { 38 | return new MarvelAuthorizer(BuildConfig.MARVEL_PUBLIC_KEY, BuildConfig.MARVEL_PRIVATE_KEY); 39 | } 40 | 41 | @Provides 42 | Endpoint provideRestEndpoint() { 43 | return new Endpoint("http://gateway.marvel.com/"); 44 | } 45 | 46 | @Provides @Singleton 47 | CharacterRepository provideDataRepository(RestDataSource restDataSource) { 48 | return restDataSource; } 49 | 50 | @Provides @Named("executor_thread") 51 | Scheduler provideExecutorThread() { 52 | return Schedulers.newThread(); 53 | } 54 | 55 | @Provides @Named("ui_thread") 56 | Scheduler provideUiThread() { 57 | return AndroidSchedulers.mainThread(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/injector/modules/AvengerInformationModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.injector.modules; 7 | 8 | import javax.inject.Named; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | import rx.Scheduler; 13 | import saulmm.avengers.CharacterDetailsUsecase; 14 | import saulmm.avengers.GetCollectionUsecase; 15 | import saulmm.avengers.injector.Activity; 16 | import saulmm.avengers.repository.CharacterRepository; 17 | import saulmm.avengers.rest.MarvelAuthorizer; 18 | 19 | @Module 20 | public class AvengerInformationModule { 21 | private final int mCharacterId; 22 | 23 | public AvengerInformationModule(int characterId) { 24 | mCharacterId = characterId; 25 | } 26 | 27 | @Provides @Activity CharacterDetailsUsecase provideGetCharacterInformationUsecase ( 28 | CharacterRepository repository, 29 | @Named("ui_thread") Scheduler uiThread, 30 | @Named("executor_thread") Scheduler executorThread) { 31 | 32 | return new CharacterDetailsUsecase(mCharacterId, repository, uiThread, executorThread); 33 | } 34 | 35 | @Provides @Activity GetCollectionUsecase provideGetCharacterComicsUsecase (CharacterRepository repository, 36 | @Named("ui_thread") Scheduler uiThread, 37 | @Named("executor_thread") Scheduler executorThread) { 38 | 39 | return new GetCollectionUsecase(mCharacterId, repository, uiThread, executorThread); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/mvp/presenters/CharacterDetailPresenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.mvp.presenters; 7 | 8 | import javax.inject.Inject; 9 | import rx.Subscription; 10 | import saulmm.avengers.CharacterDetailsUsecase; 11 | import saulmm.avengers.entities.MarvelCharacter; 12 | import saulmm.avengers.mvp.views.CharacterDetailView; 13 | import saulmm.avengers.mvp.views.View; 14 | 15 | public class CharacterDetailPresenter implements Presenter { 16 | private CharacterDetailView mCharacterDetailView; 17 | 18 | private final CharacterDetailsUsecase mGetCharacterInformationUsecase; 19 | 20 | private Subscription mCharacterSubscription; 21 | private int mCharacterId; 22 | private String mCharacterName; 23 | 24 | @Inject 25 | public CharacterDetailPresenter(CharacterDetailsUsecase getCharacterInformationUsecase) { 26 | mGetCharacterInformationUsecase = getCharacterInformationUsecase; 27 | } 28 | 29 | @Override 30 | public void onCreate() { 31 | if (mCharacterId == -1 || mCharacterName == null) 32 | throw new IllegalStateException(); 33 | 34 | mCharacterDetailView.disableScroll(); 35 | askForCharacterDetails(); 36 | } 37 | 38 | public void askForCharacterDetails() { 39 | mCharacterSubscription = mGetCharacterInformationUsecase.execute() 40 | .subscribe(this::onCharacterReceived, this::manageCharacterError); 41 | } 42 | 43 | @Override 44 | public void onStart() { 45 | // Unused 46 | } 47 | 48 | @Override 49 | public void onStop() { 50 | if (!mCharacterSubscription.isUnsubscribed()) 51 | mCharacterSubscription.unsubscribe(); 52 | } 53 | 54 | @Override 55 | public void onPause() { 56 | // Unused 57 | } 58 | 59 | @Override 60 | public void attachView(View v) { 61 | mCharacterDetailView = (CharacterDetailView) v; 62 | } 63 | 64 | public void initializePresenter(int characterId, String characterName) { 65 | mCharacterId = characterId; 66 | mCharacterName = characterName; 67 | } 68 | 69 | private void manageCharacterError(Throwable error) { 70 | } 71 | 72 | private void onCharacterReceived(MarvelCharacter character) { 73 | mCharacterDetailView.bindCharacter(character); 74 | 75 | if (character.getDescription() != null && !character.getDescription().equals("")) 76 | mCharacterDetailView.enableScroll(); 77 | } 78 | 79 | public void onComicsIndicatorPressed() { 80 | mCharacterDetailView.goToCharacterComicsView(mCharacterId); 81 | } 82 | 83 | public void onSeriesIndicatorPressed() { 84 | mCharacterDetailView.goToCharacterSeriesView(mCharacterId); 85 | } 86 | 87 | public void onStoriesIndicatorPressed() { 88 | mCharacterDetailView.goToCharacterStoriesView(mCharacterId); 89 | } 90 | 91 | public void onEventIndicatorPressed() { 92 | mCharacterDetailView.goToCharacterEventsView(mCharacterId); 93 | } 94 | 95 | public void setCharacterId(int characterId) { 96 | mCharacterId = characterId; 97 | } 98 | 99 | public void onImageReceived() { 100 | mCharacterDetailView.hideRevealViewByAlpha(); 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/mvp/presenters/CharacterListPresenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.mvp.presenters; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import javax.inject.Inject; 11 | import rx.Subscription; 12 | import saulmm.avengers.GetCharactersUsecase; 13 | import saulmm.avengers.entities.MarvelCharacter; 14 | import saulmm.avengers.mvp.views.CharacterListView; 15 | import saulmm.avengers.mvp.views.View; 16 | 17 | public class CharacterListPresenter implements Presenter { 18 | private final GetCharactersUsecase mCharactersUsecase; 19 | private boolean mIsTheCharacterRequestRunning; 20 | private Subscription mCharactersSubscription; 21 | 22 | private List mCharacters = new ArrayList<>(); 23 | private CharacterListView mAvengersView; 24 | 25 | @Inject 26 | public CharacterListPresenter(GetCharactersUsecase charactersUsecase) { 27 | mCharactersUsecase = charactersUsecase; 28 | } 29 | 30 | @Override 31 | public void onCreate() { 32 | askForCharacters(); 33 | } 34 | 35 | 36 | @Override 37 | public void onStart() { 38 | // Unused 39 | } 40 | 41 | @Override 42 | public void onStop() { 43 | // Unused 44 | } 45 | 46 | @Override 47 | public void onPause() { 48 | mAvengersView.hideLoadingMoreCharactersIndicator(); 49 | mCharactersSubscription.unsubscribe(); 50 | mIsTheCharacterRequestRunning = false; 51 | } 52 | 53 | @Override 54 | public void attachView(View v) { 55 | mAvengersView = (CharacterListView) v; 56 | } 57 | 58 | public void onListEndReached() { 59 | if (!mIsTheCharacterRequestRunning) askForNewCharacters(); 60 | } 61 | 62 | public void askForCharacters() { 63 | mIsTheCharacterRequestRunning = true; 64 | mAvengersView.hideErrorView(); 65 | mAvengersView.showLoadingView(); 66 | 67 | mCharactersSubscription = mCharactersUsecase.execute() 68 | .subscribe(this::onCharactersReceived, this::showErrorView); 69 | } 70 | 71 | public void onCharactersReceived(List characters) { 72 | mCharacters.addAll(characters); 73 | mAvengersView.bindCharacterList(mCharacters); 74 | mAvengersView.showCharacterList(); 75 | mAvengersView.hideEmptyIndicator(); 76 | mIsTheCharacterRequestRunning = false; 77 | } 78 | 79 | public void askForNewCharacters() { 80 | mAvengersView.showLoadingMoreCharactersIndicator(); 81 | mIsTheCharacterRequestRunning = true; 82 | 83 | mCharactersSubscription = mCharactersUsecase.execute() 84 | .subscribe(this::onNewCharactersReceived, this::onNewCharactersError); 85 | } 86 | 87 | private void onNewCharactersError(Throwable error) { 88 | showGenericError(); 89 | mIsTheCharacterRequestRunning = false; 90 | } 91 | 92 | private void onNewCharactersReceived(List newCharacters) { 93 | mCharacters.addAll(newCharacters); 94 | mAvengersView.updateCharacterList(GetCharactersUsecase.DEFAULT_CHARACTERS_LIMIT); 95 | 96 | mAvengersView.hideLoadingIndicator(); 97 | mIsTheCharacterRequestRunning = false; 98 | } 99 | 100 | public void showErrorView(Throwable error) { 101 | mAvengersView.showUknownErrorMessage(); 102 | mAvengersView.hideLoadingMoreCharactersIndicator(); 103 | mAvengersView.hideEmptyIndicator(); 104 | mAvengersView.hideCharactersList(); 105 | } 106 | 107 | public void showGenericError() { 108 | mAvengersView.hideLoadingIndicator(); 109 | mAvengersView.showLightError(); 110 | } 111 | 112 | public void onErrorRetryRequest() { 113 | if (mCharacters.isEmpty()) 114 | askForCharacters(); 115 | else 116 | askForNewCharacters(); 117 | } 118 | 119 | public void onElementClick(int position) { 120 | int characterId = mCharacters.get(position).getId(); 121 | String characterName = mCharacters.get(position).getName(); 122 | mAvengersView.showDetailScreen(characterName, characterId); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/mvp/presenters/CollectionPresenter.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.mvp.presenters; 2 | 3 | import android.content.Context; 4 | import java.util.List; 5 | import javax.inject.Inject; 6 | import rx.android.schedulers.AndroidSchedulers; 7 | import rx.schedulers.Schedulers; 8 | import saulmm.avengers.GetCollectionUsecase; 9 | import saulmm.avengers.entities.CollectionItem; 10 | import saulmm.avengers.mvp.views.CollectionView; 11 | import saulmm.avengers.mvp.views.View; 12 | 13 | public class CollectionPresenter implements Presenter { 14 | private final GetCollectionUsecase mGetCollectionUsecase; 15 | private final Context mActivityContext; 16 | private int mCharacterId; 17 | private String mCollectionType; 18 | private CollectionView mCollectionView; 19 | 20 | @Inject 21 | public CollectionPresenter(GetCollectionUsecase getCollectionUsecase, Context activityContext) { 22 | mGetCollectionUsecase = getCollectionUsecase; 23 | mActivityContext = activityContext; 24 | } 25 | 26 | @Override public void onStart() {} 27 | 28 | @Override public void onStop() {} 29 | 30 | @Override 31 | public void onPause() {} 32 | 33 | @Override 34 | public void attachView(View v) { 35 | mCollectionView = (CollectionView) v; 36 | } 37 | 38 | @Override 39 | public void onCreate() { 40 | mGetCollectionUsecase.setType(mCollectionType); 41 | mGetCollectionUsecase.execute() 42 | .subscribe(this::onCollectionItemsReceived); 43 | } 44 | 45 | public void initialisePresenters(String collectionType, int characterId) { 46 | mCollectionType = collectionType; 47 | mCharacterId = characterId; 48 | } 49 | 50 | private void onCollectionItemsReceived(List items) { 51 | mCollectionView.hideLoadingIndicator(); 52 | mCollectionView.showItems(items); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/mvp/presenters/Presenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.mvp.presenters; 7 | 8 | import saulmm.avengers.mvp.views.View; 9 | 10 | public interface Presenter { 11 | void onStart(); 12 | 13 | void onStop(); 14 | 15 | void onPause(); 16 | 17 | void attachView (View v); 18 | 19 | void onCreate(); 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/mvp/views/CharacterDetailView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.mvp.views; 7 | 8 | import android.graphics.Bitmap; 9 | import saulmm.avengers.entities.MarvelCharacter; 10 | 11 | public interface CharacterDetailView extends View { 12 | void disableScroll(); 13 | 14 | void hideRevealViewByAlpha(); 15 | 16 | void bindCharacter(MarvelCharacter character); 17 | 18 | void enableScroll(); 19 | 20 | void goToCharacterComicsView(int characterId); 21 | 22 | void goToCharacterSeriesView(int characterId); 23 | 24 | void goToCharacterEventsView(int characterId); 25 | 26 | void goToCharacterStoriesView(int characterId); 27 | 28 | void showError(String s); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/mvp/views/CharacterListView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.mvp.views; 7 | 8 | import android.app.ActivityOptions; 9 | import java.util.List; 10 | import saulmm.avengers.entities.MarvelCharacter; 11 | 12 | @SuppressWarnings("unused") 13 | public interface CharacterListView extends View { 14 | 15 | void bindCharacterList(List avengers); 16 | 17 | void showCharacterList(); 18 | 19 | void hideCharactersList(); 20 | 21 | void showLoadingMoreCharactersIndicator(); 22 | 23 | void hideLoadingMoreCharactersIndicator(); 24 | 25 | void hideLoadingIndicator (); 26 | 27 | void showLoadingView(); 28 | 29 | void hideLoadingView(); 30 | 31 | void showLightError(); 32 | 33 | void hideErrorView(); 34 | 35 | void showEmptyIndicator(); 36 | 37 | void hideEmptyIndicator(); 38 | 39 | void updateCharacterList(int charactersLimit); 40 | 41 | void showConnectionErrorMessage(); 42 | 43 | void showServerErrorMessage(); 44 | 45 | void showUknownErrorMessage(); 46 | 47 | void showDetailScreen(String characterName, int characterId); 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/mvp/views/CollectionView.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.mvp.views; 2 | 3 | import java.util.List; 4 | import saulmm.avengers.entities.CollectionItem; 5 | 6 | public interface CollectionView extends View { 7 | 8 | void showLoadingIndicator(); 9 | 10 | void hideLoadingIndicator(); 11 | 12 | void showItems(List items); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/mvp/views/View.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.mvp.views; 7 | 8 | public interface View { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/utils/DatabindingUtils.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.utils; 2 | 3 | import android.databinding.BindingAdapter; 4 | import android.view.View; 5 | 6 | public class DatabindingUtils { 7 | @BindingAdapter({"app:onClick"}) 8 | public static void bindOnClick(View view, final Runnable runnable) { 9 | view.setOnClickListener(v -> runnable.run()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/utils/OnCharacterImageCallback.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.utils; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | public interface OnCharacterImageCallback { 6 | void onReceive(Bitmap bitmap); 7 | } -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/utils/TransitionUtils.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.utils; 2 | 3 | import android.transition.Explode; 4 | import android.transition.Slide; 5 | import android.transition.Transition; 6 | 7 | public class TransitionUtils { 8 | 9 | public static Transition buildExplodeTransition (Integer... exlcudeIds) { 10 | Explode explodeTransition = new Explode(); 11 | excludeTransitionIds(explodeTransition, exlcudeIds); 12 | return explodeTransition; 13 | } 14 | 15 | public static Transition buildSlideTransition (int gravity, Integer... excludeIds) { 16 | Slide explodeTransition = new Slide(); 17 | excludeTransitionIds(explodeTransition, excludeIds); 18 | return explodeTransition; 19 | } 20 | 21 | private static void excludeTransitionIds(Transition transition, Integer[] exlcudeIds) { 22 | transition.excludeTarget(android.R.id.statusBarBackground, true); 23 | transition.excludeTarget(android.R.id.navigationBarBackground, true); 24 | 25 | for (Integer exlcudeId : exlcudeIds) { 26 | transition.excludeTarget(exlcudeId, true); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.utils; 2 | 3 | public class Utils { 4 | 5 | public final static String CHARACTER_IMG_NAME = "ch_thumb"; 6 | public final static String BANGERS_TTF = "fonts/Bangers.ttf"; 7 | 8 | public static final int DIALOG_ACCEPT = -1; 9 | public static final int DIALOG_CANCEL = -2; 10 | 11 | public static String getListTransitionName (int position) { 12 | return CHARACTER_IMG_NAME + position; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/views/RecyclerClickListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.views; 7 | 8 | import android.view.View; 9 | import android.widget.ImageView; 10 | 11 | public interface RecyclerClickListener { 12 | 13 | void onElementClick(int position, View sharedView, ImageView characterImageView); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/views/activities/CharacterDetailActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.views.activities; 7 | 8 | import android.animation.ValueAnimator; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.databinding.BindingAdapter; 12 | import android.databinding.DataBindingUtil; 13 | import android.graphics.Bitmap; 14 | import android.os.Bundle; 15 | import android.support.design.widget.AppBarLayout; 16 | import android.support.design.widget.CollapsingToolbarLayout; 17 | import android.support.v7.app.AlertDialog; 18 | import android.support.v7.app.AppCompatActivity; 19 | import android.support.v7.graphics.Palette; 20 | import android.transition.Transition; 21 | import android.view.Gravity; 22 | import android.view.View; 23 | import android.view.ViewTreeObserver; 24 | import android.widget.ImageView; 25 | 26 | import butterknife.Bind; 27 | import butterknife.BindColor; 28 | import butterknife.BindInt; 29 | import butterknife.ButterKnife; 30 | import com.bumptech.glide.Glide; 31 | import com.bumptech.glide.request.animation.GlideAnimation; 32 | import com.bumptech.glide.request.target.BitmapImageViewTarget; 33 | import javax.inject.Inject; 34 | import saulmm.avengers.AvengersApplication; 35 | import saulmm.avengers.R; 36 | import saulmm.avengers.databinding.ActivityAvengerDetailBinding; 37 | import saulmm.avengers.entities.CollectionItem; 38 | import saulmm.avengers.entities.MarvelCharacter; 39 | import saulmm.avengers.injector.components.DaggerAvengerInformationComponent; 40 | import saulmm.avengers.injector.modules.ActivityModule; 41 | import saulmm.avengers.injector.modules.AvengerInformationModule; 42 | import saulmm.avengers.mvp.presenters.CharacterDetailPresenter; 43 | import saulmm.avengers.mvp.views.CharacterDetailView; 44 | import saulmm.avengers.utils.OnCharacterImageCallback; 45 | import saulmm.avengers.utils.TransitionUtils; 46 | import saulmm.avengers.views.utils.AnimUtils; 47 | 48 | public class CharacterDetailActivity extends AppCompatActivity implements CharacterDetailView { 49 | private static final String EXTRA_CHARACTER_NAME = "character.name"; 50 | public static final String EXTRA_CHARACTER_ID = "character.id"; 51 | 52 | @Bind(R.id.character_collapsing) CollapsingToolbarLayout mCollapsing; 53 | @BindInt(R.integer.duration_medium) int mAnimMediumDuration; 54 | @BindInt(R.integer.duration_huge) int mAnimHugeDuration; 55 | @BindColor(R.color.brand_primary_dark) int mColorPrimaryDark; 56 | 57 | @Inject CharacterDetailPresenter mCharacterDetailPresenter; 58 | private ActivityAvengerDetailBinding mBinding; 59 | 60 | private OnCharacterImageCallback onCharacterImageCallback = new OnCharacterImageCallback() { 61 | 62 | @Override 63 | public void onReceive(Bitmap bitmap) { 64 | initActivityColors(bitmap); 65 | } 66 | }; 67 | 68 | private int mScrolleableAppbarLayoutFlags; 69 | 70 | @Override 71 | public void onCreate(Bundle savedInstanceState) { 72 | super.onCreate(savedInstanceState); 73 | initializeDependencyInjector(); 74 | initializeBinding(); 75 | initButterknife(); 76 | initializePresenter(); 77 | initToolbar(); 78 | initTransitions(); 79 | } 80 | 81 | @Override 82 | public void disableScroll() { 83 | mScrolleableAppbarLayoutFlags = ((AppBarLayout.LayoutParams) mCollapsing.getLayoutParams()).getScrollFlags(); 84 | AppBarLayout.LayoutParams layoutParams = ((AppBarLayout.LayoutParams) mCollapsing.getLayoutParams()); 85 | layoutParams.setScrollFlags(0); 86 | mCollapsing.setLayoutParams(layoutParams); 87 | } 88 | 89 | private void initializeBinding() { 90 | mBinding = DataBindingUtil.setContentView( 91 | this, R.layout.activity_avenger_detail); 92 | 93 | mBinding.setImageCallback(onCharacterImageCallback); 94 | } 95 | 96 | 97 | public void initActivityColors(Bitmap sourceBitmap) { 98 | Palette.from(sourceBitmap) 99 | .generate(palette -> { 100 | View statsRevealView = mBinding.characterStatsReveal; 101 | View imageCover = mBinding.characterImageReveal; 102 | int darkVibrant = palette.getDarkVibrantColor(mColorPrimaryDark); 103 | 104 | mBinding.setDarkSwatch(palette.getDarkVibrantSwatch()); 105 | 106 | ValueAnimator colorAnimation = ValueAnimator.ofArgb(mColorPrimaryDark, darkVibrant); 107 | colorAnimation.setDuration(mAnimHugeDuration); 108 | colorAnimation.addUpdateListener(animator -> { 109 | imageCover.setBackgroundColor((Integer) animator.getAnimatedValue()); 110 | }); 111 | 112 | colorAnimation.start(); 113 | 114 | statsRevealView.getViewTreeObserver().addOnGlobalLayoutListener( 115 | new ViewTreeObserver.OnGlobalLayoutListener() { 116 | @Override public void onGlobalLayout() { 117 | statsRevealView.getViewTreeObserver() 118 | .removeOnGlobalLayoutListener(this); 119 | 120 | AnimUtils.showRevealEffect(statsRevealView, 121 | statsRevealView.getWidth() / 2, 0, null); 122 | 123 | } 124 | }); 125 | 126 | getWindow().setStatusBarColor(darkVibrant); 127 | }); 128 | } 129 | 130 | 131 | private void initButterknife() { 132 | ButterKnife.bind(this); 133 | } 134 | 135 | @Override 136 | protected void onStart() { 137 | super.onStart(); 138 | mCharacterDetailPresenter.onStart(); 139 | } 140 | 141 | private void initializePresenter() { 142 | int characterId = getIntent().getIntExtra(EXTRA_CHARACTER_ID, -1); 143 | String characterName = getIntent().getStringExtra(EXTRA_CHARACTER_NAME); 144 | 145 | mCharacterDetailPresenter.attachView(this); 146 | mCharacterDetailPresenter.setCharacterId(characterId); 147 | mCharacterDetailPresenter.initializePresenter(characterId, characterName); 148 | mCharacterDetailPresenter.onCreate(); 149 | mBinding.setPresenter(mCharacterDetailPresenter); 150 | } 151 | 152 | private void initializeDependencyInjector() { 153 | AvengersApplication avengersApplication = (AvengersApplication) getApplication(); 154 | 155 | int avengerId = getIntent().getIntExtra(EXTRA_CHARACTER_ID, -1); 156 | 157 | DaggerAvengerInformationComponent.builder() 158 | .activityModule(new ActivityModule(this)) 159 | .appComponent(avengersApplication.getAppComponent()) 160 | .avengerInformationModule(new AvengerInformationModule(avengerId)) 161 | .build().inject(this); 162 | } 163 | 164 | private void initTransitions() { 165 | final String sharedViewName = getIntent().getStringExtra( 166 | CharacterListActivity.EXTRA_IMAGE_TRANSITION_NAME); 167 | 168 | String characterTitle = getIntent().getStringExtra( 169 | CharacterListActivity.EXTRA_CHARACTER_NAME); 170 | 171 | Transition enterTransition = TransitionUtils.buildSlideTransition(Gravity.BOTTOM); 172 | enterTransition.setDuration(mAnimMediumDuration); 173 | 174 | getWindow().setEnterTransition(enterTransition); 175 | getWindow().setReturnTransition(TransitionUtils.buildSlideTransition(Gravity.BOTTOM)); 176 | 177 | View collapsingToolbar = mBinding.characterCollapsing; 178 | View imageReveal = mBinding.characterImageReveal; 179 | collapsingToolbar.getViewTreeObserver().addOnGlobalLayoutListener( 180 | new ViewTreeObserver.OnGlobalLayoutListener() { 181 | @Override public void onGlobalLayout() { 182 | collapsingToolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this); 183 | int width = imageReveal.getWidth(); 184 | int height = imageReveal.getHeight(); 185 | 186 | AnimUtils.showRevealEffect(imageReveal, width / 2, height / 2, null); 187 | } 188 | }); 189 | } 190 | 191 | @Override 192 | public void hideRevealViewByAlpha() { 193 | mBinding.characterImageReveal.animate().alpha(0f).setDuration( 194 | mAnimHugeDuration).start(); 195 | } 196 | 197 | private void initToolbar() { 198 | mBinding.characterToolbar.setNavigationOnClickListener(v -> onBackPressed()); 199 | } 200 | 201 | @Override 202 | public void showError(String errorMessage) { 203 | new AlertDialog.Builder(this) 204 | .setTitle(getString(R.string.dialog_error)) 205 | .setPositiveButton(getString(R.string.action_accept), (dialog, which) -> finish()) 206 | .setMessage(errorMessage) 207 | .setCancelable(false).show(); 208 | } 209 | 210 | @Override 211 | public void bindCharacter(MarvelCharacter character) { 212 | mBinding.setCharacter(character); 213 | } 214 | 215 | @Override 216 | public void enableScroll() { 217 | AppBarLayout.LayoutParams layoutParams = (AppBarLayout.LayoutParams) mCollapsing.getLayoutParams(); 218 | layoutParams.setScrollFlags(mScrolleableAppbarLayoutFlags); 219 | mCollapsing.setLayoutParams(layoutParams); 220 | } 221 | 222 | @Override 223 | public void goToCharacterComicsView(int characterId) { 224 | CollectionActivity.start(this, characterId, CollectionItem.COMICS); 225 | } 226 | 227 | @Override 228 | public void goToCharacterSeriesView(int characterId) { 229 | CollectionActivity.start(this, characterId, CollectionItem.SERIES); 230 | } 231 | 232 | @Override 233 | public void goToCharacterEventsView(int characterId) { 234 | CollectionActivity.start(this, characterId, CollectionItem.EVENTS); 235 | } 236 | 237 | @Override 238 | public void goToCharacterStoriesView(int characterId) { 239 | CollectionActivity.start(this, characterId, CollectionItem.STORIES); 240 | } 241 | 242 | @BindingAdapter({"source", "presenter", "callback"}) 243 | public static void setImageSource(ImageView v, String url, 244 | CharacterDetailPresenter detailPresenter, OnCharacterImageCallback imageCallback) { 245 | 246 | Glide.with(v.getContext()).load(url).asBitmap().into(new BitmapImageViewTarget(v) { 247 | @Override public void onResourceReady(Bitmap resource, 248 | GlideAnimation glideAnimation) { 249 | super.onResourceReady(resource, glideAnimation); 250 | v.setImageBitmap(resource); 251 | imageCallback.onReceive(resource); 252 | detailPresenter.onImageReceived(); 253 | 254 | } 255 | }); 256 | } 257 | 258 | @Override 259 | protected void onStop() { 260 | super.onStop(); 261 | mCharacterDetailPresenter.onStop(); 262 | } 263 | 264 | public static void start(Context context, String characterName, int characterId) { 265 | Intent characterDetailItent = new Intent(context, CharacterDetailActivity.class); 266 | characterDetailItent.putExtra(EXTRA_CHARACTER_NAME, characterName); 267 | characterDetailItent.putExtra(EXTRA_CHARACTER_ID, characterId); 268 | context.startActivity(characterDetailItent); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/views/activities/CharacterListActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.views.activities; 7 | 8 | import android.app.ActivityOptions; 9 | import android.os.Bundle; 10 | import android.support.design.widget.CollapsingToolbarLayout; 11 | import android.support.design.widget.Snackbar; 12 | import android.support.v7.app.AppCompatActivity; 13 | import android.support.v7.widget.LinearLayoutManager; 14 | import android.support.v7.widget.RecyclerView; 15 | import android.support.v7.widget.RecyclerView.OnScrollListener; 16 | import android.support.v7.widget.Toolbar; 17 | import android.view.View; 18 | import android.widget.ProgressBar; 19 | import android.widget.TextView; 20 | import butterknife.Bind; 21 | import butterknife.ButterKnife; 22 | import butterknife.OnClick; 23 | import java.util.List; 24 | import javax.inject.Inject; 25 | import saulmm.avengers.AvengersApplication; 26 | import saulmm.avengers.R; 27 | import saulmm.avengers.entities.MarvelCharacter; 28 | import saulmm.avengers.injector.components.DaggerAvengersComponent; 29 | import saulmm.avengers.injector.modules.ActivityModule; 30 | 31 | import saulmm.avengers.mvp.presenters.CharacterListPresenter; 32 | import saulmm.avengers.mvp.views.CharacterListView; 33 | import saulmm.avengers.utils.Utils; 34 | import saulmm.avengers.views.adapter.AvengersListAdapter; 35 | import saulmm.avengers.views.views.RecyclerInsetsDecoration; 36 | 37 | 38 | public class CharacterListActivity extends AppCompatActivity 39 | implements CharacterListView { 40 | 41 | public final static String EXTRA_CHARACTER_NAME = "character_name"; 42 | public final static String EXTRA_IMAGE_TRANSITION_NAME = "transition_name"; 43 | 44 | @Bind(R.id.activity_avengers_recycler) RecyclerView mAvengersRecycler; 45 | @Bind(R.id.activity_avengers_toolbar) Toolbar mAvengersToolbar; 46 | @Bind(R.id.activity_avengers_progress) ProgressBar mAvengersProgress; 47 | @Bind(R.id.activity_avengers_collapsing) CollapsingToolbarLayout mCollapsingToolbar; 48 | @Bind(R.id.activity_avengers_empty_indicator) View mEmptyIndicator; 49 | @Bind(R.id.activity_avengers_error_view) View mErrorView; 50 | 51 | @Inject CharacterListPresenter mAvengersListPresenter; 52 | 53 | private AvengersListAdapter mCharacterListAdapter; 54 | private Snackbar mLoadingMoreCharactersSnack; 55 | 56 | @Override 57 | protected void onCreate(Bundle savedInstanceState) { 58 | super.onCreate(savedInstanceState); 59 | initUi(); 60 | initializeToolbar(); 61 | initializeRecyclerView(); 62 | initializeDependencyInjector(); 63 | initializePresenter(); 64 | } 65 | 66 | private void initUi() { 67 | setContentView(R.layout.activity_avengers_list); 68 | ButterKnife.bind(this); 69 | } 70 | 71 | private void initializeToolbar() { 72 | mCollapsingToolbar.setTitle(""); 73 | } 74 | 75 | @OnClick(R.id.view_error_retry_button) 76 | public void onRetryButtonClicked(View v) { 77 | mAvengersListPresenter.onErrorRetryRequest(); 78 | } 79 | 80 | @Override 81 | protected void onStart() { 82 | super.onStart(); 83 | mAvengersListPresenter.onStart(); 84 | } 85 | 86 | @Override 87 | protected void onPause() { 88 | super.onPause(); 89 | mAvengersListPresenter.onPause(); 90 | } 91 | 92 | private void initializePresenter() { 93 | mAvengersListPresenter.attachView(this); 94 | mAvengersListPresenter.onCreate(); 95 | } 96 | 97 | private void initializeDependencyInjector() { 98 | AvengersApplication avengersApplication = (AvengersApplication) getApplication(); 99 | 100 | DaggerAvengersComponent.builder() 101 | .activityModule(new ActivityModule(this)) 102 | .appComponent(avengersApplication.getAppComponent()) 103 | .build().inject(this); 104 | } 105 | 106 | private void initializeRecyclerView() { 107 | mAvengersRecycler.setLayoutManager(new LinearLayoutManager(this)); 108 | mAvengersRecycler.addItemDecoration(new RecyclerInsetsDecoration(this)); 109 | mAvengersRecycler.addOnScrollListener(mOnScrollListener); 110 | } 111 | 112 | @Override 113 | public void bindCharacterList(List avengers) { 114 | mCharacterListAdapter = new AvengersListAdapter(avengers, this, 115 | (position, sharedView, characterImageView) -> { 116 | mAvengersListPresenter.onElementClick(position); 117 | }); 118 | 119 | mAvengersRecycler.setAdapter(mCharacterListAdapter); 120 | } 121 | 122 | @Override 123 | public void showCharacterList() { 124 | if (mAvengersRecycler.getVisibility() == View.GONE || 125 | mAvengersRecycler.getVisibility() == View.INVISIBLE) 126 | 127 | mAvengersRecycler.setVisibility(View.VISIBLE); 128 | } 129 | 130 | @Override 131 | public void updateCharacterList(int charactersAdded) { 132 | mCharacterListAdapter.notifyItemRangeInserted( 133 | mCharacterListAdapter.getItemCount() + charactersAdded, charactersAdded); 134 | } 135 | 136 | @Override 137 | public void hideCharactersList() { 138 | mAvengersRecycler.setVisibility(View.GONE); 139 | } 140 | 141 | @Override 142 | public void showLoadingMoreCharactersIndicator() { 143 | mLoadingMoreCharactersSnack = Snackbar.make(mAvengersRecycler, 144 | getString(R.string.message_loading_more_characters), Snackbar.LENGTH_INDEFINITE); 145 | 146 | mLoadingMoreCharactersSnack.show(); 147 | } 148 | 149 | @Override 150 | public void hideLoadingMoreCharactersIndicator() { 151 | if (mLoadingMoreCharactersSnack != null) mLoadingMoreCharactersSnack.dismiss(); 152 | } 153 | 154 | @Override 155 | public void hideLoadingIndicator() { 156 | mLoadingMoreCharactersSnack.dismiss(); 157 | } 158 | 159 | @Override 160 | public void showLoadingView() { 161 | mEmptyIndicator.setVisibility(View.VISIBLE); 162 | } 163 | 164 | @Override 165 | public void hideLoadingView() { 166 | mEmptyIndicator.setVisibility(View.GONE); 167 | } 168 | 169 | @Override 170 | public void showLightError() { 171 | Snackbar.make(mAvengersRecycler, getString(R.string.error_loading_characters), Snackbar.LENGTH_LONG) 172 | .setAction(R.string.try_again, v -> mAvengersListPresenter.onErrorRetryRequest()) 173 | .show(); 174 | } 175 | 176 | @Override 177 | public void hideErrorView() { 178 | mErrorView.setVisibility(View.GONE); 179 | } 180 | 181 | @Override 182 | public void showEmptyIndicator() { 183 | mEmptyIndicator.setVisibility(View.VISIBLE); 184 | } 185 | 186 | @Override 187 | public void hideEmptyIndicator() { 188 | mEmptyIndicator.setVisibility(View.GONE); 189 | } 190 | 191 | @Override 192 | protected void onStop() { 193 | super.onStop(); 194 | mAvengersListPresenter.onStop(); 195 | } 196 | 197 | private OnScrollListener mOnScrollListener = new OnScrollListener() { 198 | @Override 199 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 200 | LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); 201 | int visibleItemsCount = layoutManager.getChildCount(); 202 | int totalItemsCount = layoutManager.getItemCount(); 203 | int firstVisibleItemPos = layoutManager.findFirstVisibleItemPosition(); 204 | 205 | if (visibleItemsCount + firstVisibleItemPos >= totalItemsCount) { 206 | mAvengersListPresenter.onListEndReached(); 207 | } 208 | } 209 | }; 210 | 211 | @Override 212 | public void showConnectionErrorMessage() { 213 | TextView errorTextView = ButterKnife.findById(mErrorView, R.id.view_error_message); 214 | errorTextView.setText(R.string.error_network_uknownhost); 215 | mErrorView.setVisibility(View.VISIBLE); 216 | } 217 | 218 | @Override 219 | public void showServerErrorMessage() { 220 | TextView errorTextView = ButterKnife.findById(mErrorView, R.id.view_error_message); 221 | errorTextView.setText(R.string.error_network_marvel_server); 222 | mErrorView.setVisibility(View.VISIBLE); 223 | } 224 | 225 | @Override 226 | public void showUknownErrorMessage() { 227 | TextView errorTextView = ButterKnife.findById(mErrorView, R.id.view_error_message); 228 | errorTextView.setText("Uknown error"); 229 | mErrorView.setVisibility(View.VISIBLE); 230 | } 231 | 232 | @Override 233 | public void showDetailScreen(String characterName, int characterId) { 234 | CharacterDetailActivity.start(this, characterName, characterId); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/views/activities/CollectionActivity.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.views.activities; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.ImageView; 12 | import android.widget.ProgressBar; 13 | import android.widget.TextView; 14 | import butterknife.Bind; 15 | import butterknife.ButterKnife; 16 | import com.bumptech.glide.Glide; 17 | import java.util.List; 18 | import javax.inject.Inject; 19 | import saulmm.avengers.AvengersApplication; 20 | import saulmm.avengers.R; 21 | import saulmm.avengers.entities.CollectionItem; 22 | import saulmm.avengers.injector.components.DaggerAvengerInformationComponent; 23 | import saulmm.avengers.injector.modules.ActivityModule; 24 | import saulmm.avengers.injector.modules.AvengerInformationModule; 25 | import saulmm.avengers.mvp.presenters.CollectionPresenter; 26 | import saulmm.avengers.mvp.views.CollectionView; 27 | 28 | public class CollectionActivity extends AppCompatActivity implements CollectionView { 29 | private final static String EXTRA_CHARACTER_ID = "character_id"; 30 | private final static String EXTRA_COLLECTION_TYPE = "collection_type"; 31 | 32 | @Bind(R.id.collection_list) RecyclerView mCollectionRecycler; 33 | @Bind(R.id.collection_loading) ProgressBar mLoadingIndicator; 34 | @Inject CollectionPresenter mCollectionPresenter; 35 | 36 | @Override 37 | public void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_character_collection); 40 | ButterKnife.bind(this); 41 | 42 | initDependencyInjector(); 43 | initializePresenter(); 44 | } 45 | 46 | private void initDependencyInjector() { 47 | int avengerId = getIntent().getIntExtra(EXTRA_CHARACTER_ID, -1); 48 | 49 | DaggerAvengerInformationComponent.builder() 50 | .activityModule(new ActivityModule(this)) 51 | .appComponent(((AvengersApplication) getApplication()).getAppComponent()) 52 | .avengerInformationModule(new AvengerInformationModule(avengerId)) 53 | .build().inject(this); 54 | } 55 | 56 | private void initializePresenter() { 57 | int characterId = getIntent().getIntExtra(EXTRA_CHARACTER_ID, -1); 58 | String collectionType = getIntent().getStringExtra(EXTRA_COLLECTION_TYPE); 59 | mCollectionPresenter.attachView(this); 60 | mCollectionPresenter.initialisePresenters(collectionType, characterId); 61 | mCollectionPresenter.onCreate(); 62 | } 63 | 64 | @Override 65 | public void showLoadingIndicator() { 66 | mLoadingIndicator.setVisibility(View.VISIBLE); 67 | } 68 | 69 | @Override 70 | public void hideLoadingIndicator() { 71 | mLoadingIndicator.setVisibility(View.GONE); 72 | } 73 | 74 | @Override 75 | public void showItems(List items) { 76 | mCollectionRecycler.setAdapter(new CollectionAdapter(items)); 77 | } 78 | 79 | public static void start(Context context, int characterId, String type) { 80 | Intent collectionIntent = new Intent(context, CollectionActivity.class); 81 | collectionIntent.putExtra(EXTRA_CHARACTER_ID, characterId); 82 | collectionIntent.putExtra(EXTRA_COLLECTION_TYPE, type); 83 | context.startActivity(collectionIntent); 84 | } 85 | 86 | 87 | private class CollectionAdapter extends RecyclerView.Adapter { 88 | private List mCollectionItems; 89 | 90 | public CollectionAdapter(List collectionItems) { 91 | mCollectionItems = collectionItems; 92 | } 93 | 94 | @Override 95 | public CollectionItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 96 | View rootView = LayoutInflater.from(parent.getContext()).inflate( 97 | R.layout.item_comic, parent, false); 98 | 99 | return new CollectionItemViewHolder(rootView); 100 | } 101 | 102 | @Override 103 | public void onBindViewHolder(CollectionItemViewHolder holder, int position) { 104 | holder.bindItem(mCollectionItems.get(position)); 105 | } 106 | 107 | @Override public int getItemCount() { 108 | return mCollectionItems.size(); 109 | } 110 | } 111 | 112 | class CollectionItemViewHolder extends RecyclerView.ViewHolder { 113 | @Bind(R.id.item_title) TextView itemTitleTextView; 114 | @Bind(R.id.item_image) ImageView itemImageView; 115 | @Bind(R.id.item_text) TextView itemTextTextView; 116 | 117 | public CollectionItemViewHolder(View itemView) { 118 | super(itemView); 119 | ButterKnife.bind(this, itemView); 120 | } 121 | 122 | public void bindItem(CollectionItem collectionItem) { 123 | itemTitleTextView.setText(collectionItem.getTitle()); 124 | itemTextTextView.setText(collectionItem.getDescription()); 125 | 126 | if (collectionItem.getThumbnail() != null) { 127 | Glide.with(CollectionActivity.this) 128 | .load(collectionItem.getThumbnail().getImageUrl()) 129 | .error(R.drawable.error_placeholder) 130 | .into(itemImageView); 131 | 132 | } else { 133 | itemTextTextView.setVisibility(View.GONE); 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/views/adapter/AvengersListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | package saulmm.avengers.views.adapter; 7 | 8 | import android.content.Context; 9 | import android.graphics.drawable.ColorDrawable; 10 | import android.support.v7.widget.RecyclerView; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.ImageView; 15 | import android.widget.TextView; 16 | import butterknife.Bind; 17 | import butterknife.BindColor; 18 | import butterknife.ButterKnife; 19 | import com.bumptech.glide.Glide; 20 | import java.util.List; 21 | import saulmm.avengers.R; 22 | import saulmm.avengers.entities.MarvelCharacter; 23 | import saulmm.avengers.utils.Utils; 24 | import saulmm.avengers.views.RecyclerClickListener; 25 | 26 | public class AvengersListAdapter extends RecyclerView.Adapter { 27 | private final String NOT_AVAILABLE_URL = "http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg"; 28 | private final RecyclerClickListener mRecyclerListener; 29 | private final List mCharacters; 30 | 31 | private Context mContext; 32 | 33 | public AvengersListAdapter(List avengers, Context context, 34 | RecyclerClickListener recyclerClickListener) { 35 | 36 | mCharacters = avengers; 37 | mContext = context; 38 | mRecyclerListener = recyclerClickListener; 39 | } 40 | 41 | @Override 42 | public CharacterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 43 | View rootView = LayoutInflater.from(mContext).inflate( 44 | R.layout.item_character, parent, false); 45 | 46 | return new CharacterViewHolder(rootView, mRecyclerListener); 47 | } 48 | 49 | @Override 50 | public void onBindViewHolder(CharacterViewHolder holder, int position) { 51 | holder.bindAvenger(mCharacters.get(position)); 52 | } 53 | 54 | @Override 55 | public int getItemCount() { 56 | return mCharacters.size(); 57 | } 58 | 59 | public class CharacterViewHolder extends RecyclerView.ViewHolder { 60 | @Bind(R.id.item_avenger_title) TextView avengerTitleTextView; 61 | @Bind(R.id.item_avenger_thumb) ImageView avengerThumbImageView; 62 | @Bind(R.id.item_avenger_placeholder_name) TextView avengerPlaceholderTitleTextView; 63 | @BindColor(R.color.brand_primary) int mColorPrimary; 64 | 65 | public CharacterViewHolder(View itemView, final RecyclerClickListener recyclerClickListener) { 66 | super(itemView); 67 | ButterKnife.bind(this, itemView); 68 | bindListener(itemView, recyclerClickListener); 69 | } 70 | 71 | public void bindAvenger(MarvelCharacter character) { 72 | avengerTitleTextView.setText(character.getName()); 73 | avengerTitleTextView.setTransitionName(Utils.getListTransitionName(getPosition())); 74 | 75 | if (character.getImageUrl().equals(NOT_AVAILABLE_URL)) { 76 | ColorDrawable colorDrawable = new ColorDrawable(mColorPrimary); 77 | avengerThumbImageView.setDrawingCacheEnabled(true); 78 | avengerThumbImageView.setImageDrawable(colorDrawable); 79 | 80 | } else { 81 | Glide.with(mContext) 82 | .load(character.getImageUrl()) 83 | .crossFade() 84 | .into(avengerThumbImageView); 85 | } 86 | } 87 | 88 | private void bindListener(View itemView, final RecyclerClickListener recyclerClickListener) { 89 | itemView.setOnClickListener(v -> 90 | recyclerClickListener.onElementClick(getPosition(), avengerTitleTextView, avengerThumbImageView)); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/views/utils/AnimUtils.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.views.utils; 2 | 3 | import android.animation.Animator; 4 | import android.support.annotation.Nullable; 5 | import android.view.View; 6 | import android.view.ViewAnimationUtils; 7 | 8 | public class AnimUtils { 9 | 10 | public static final int REVEAL_DURATION = 500; 11 | 12 | public static void showRevealEffect(final View v, int centerX, int centerY, 13 | @Nullable Animator.AnimatorListener lis) { 14 | 15 | v.setVisibility(View.VISIBLE); 16 | 17 | int finalRadius = Math.max(v.getWidth(), v.getHeight()); 18 | 19 | Animator anim = ViewAnimationUtils.createCircularReveal( 20 | v, centerX, centerY, 0, finalRadius); 21 | 22 | anim.setDuration(REVEAL_DURATION); 23 | 24 | if (lis != null) 25 | anim.addListener(lis); 26 | 27 | anim.start(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/views/utils/TransitionListenerAdapter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package saulmm.avengers.views.utils; 15 | 16 | import android.transition.Transition; 17 | 18 | 19 | public class TransitionListenerAdapter implements Transition.TransitionListener { 20 | 21 | @Override 22 | public void onTransitionStart(Transition transition) { 23 | 24 | } 25 | 26 | @Override 27 | public void onTransitionEnd(Transition transition) { 28 | 29 | } 30 | 31 | @Override 32 | public void onTransitionCancel(Transition transition) { 33 | 34 | } 35 | 36 | @Override 37 | public void onTransitionPause(Transition transition) { 38 | 39 | } 40 | 41 | @Override 42 | public void onTransitionResume(Transition transition) { 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/views/views/CustomTextView.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.views.views; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Typeface; 6 | import android.util.AttributeSet; 7 | import android.widget.TextView; 8 | import saulmm.avengers.R; 9 | 10 | public class CustomTextView extends TextView { 11 | public CustomTextView(Context context) { 12 | super(context); 13 | } 14 | 15 | public CustomTextView(Context context, AttributeSet attrs) { 16 | super(context, attrs); 17 | init(context, attrs); 18 | } 19 | 20 | public CustomTextView(Context context, AttributeSet attrs, int defStyleAttr) { 21 | super(context, attrs, defStyleAttr); 22 | init(context, attrs); 23 | } 24 | 25 | private void init(Context context, AttributeSet attrs) { 26 | TypedArray a = context.getTheme().obtainStyledAttributes(attrs, 27 | R.styleable.CustomTextView, 0, 0); 28 | 29 | String typefaceName = a.getString(R.styleable.CustomTextView_typeface); 30 | 31 | if (typefaceName != null && !typefaceName.equals("")) { 32 | setTypeface(Typeface.createFromAsset(context.getAssets(), typefaceName)); 33 | } 34 | 35 | a.recycle(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/views/views/RecyclerInsetsDecoration.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.views.views; 2 | 3 | import android.content.Context; 4 | import android.graphics.Rect; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.View; 7 | 8 | import saulmm.avengers.R; 9 | 10 | 11 | /** 12 | * ItemDecoration implementation that applies an inset margin 13 | * around each child of the RecyclerView. The inset value is controlled 14 | * by a dimension resource. 15 | * 16 | * by Dave Smith at: https://github.com/devunwired/recyclerview-playground 17 | */ 18 | public class RecyclerInsetsDecoration extends RecyclerView.ItemDecoration { 19 | 20 | private int mInsets; 21 | 22 | public RecyclerInsetsDecoration(Context context) { 23 | mInsets = context.getResources().getDimensionPixelSize(R.dimen.insets); 24 | } 25 | 26 | @Override 27 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 28 | 29 | //We can supply forced insets for each item view here in the Rect 30 | super.getItemOffsets(outRect, view, parent, state); 31 | outRect.set(mInsets, mInsets, mInsets, mInsets); 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/saulmm/avengers/views/views/SimpleDivider.java: -------------------------------------------------------------------------------- 1 | package saulmm.avengers.views.views; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.drawable.Drawable; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.View; 8 | 9 | import saulmm.avengers.R; 10 | 11 | public class SimpleDivider extends RecyclerView.ItemDecoration { 12 | 13 | private final int mPadding; 14 | 15 | private Drawable mDivider; 16 | 17 | public SimpleDivider(Context context) { 18 | 19 | mDivider = context.getDrawable(R.drawable.divider); 20 | mPadding = context.getResources().getDimensionPixelOffset(R.dimen.spacing_normal); 21 | } 22 | 23 | @Override 24 | public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { 25 | 26 | super.onDrawOver(c, parent, state); 27 | 28 | int left = mPadding; 29 | int right = parent.getWidth() - mPadding; 30 | 31 | int childCount = parent.getChildCount(); 32 | 33 | for (int i = 0; i < childCount; i++) { 34 | 35 | View child = parent.getChildAt(i); 36 | 37 | RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child 38 | .getLayoutParams(); 39 | 40 | int top = child.getBottom() + params.bottomMargin; 41 | int bottom = top + mDivider.getIntrinsicHeight(); 42 | 43 | mDivider.setBounds(left, top, right, bottom); 44 | mDivider.draw(c); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saulmm/Avengers/2b02f19d5a0ca3e36b5370f545036b30fd62a114/app/src/main/res/drawable-hdpi/ic_action_favorite.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saulmm/Avengers/2b02f19d5a0ca3e36b5370f545036b30fd62a114/app/src/main/res/drawable-mdpi/ic_action_favorite.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/avengers_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saulmm/Avengers/2b02f19d5a0ca3e36b5370f545036b30fd62a114/app/src/main/res/drawable-nodpi/avengers_header.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/error_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saulmm/Avengers/2b02f19d5a0ca3e36b5370f545036b30fd62a114/app/src/main/res/drawable-nodpi/error_placeholder.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/header_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saulmm/Avengers/2b02f19d5a0ca3e36b5370f545036b30fd62a114/app/src/main/res/drawable-nodpi/header_background.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/marvel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saulmm/Avengers/2b02f19d5a0ca3e36b5370f545036b30fd62a114/app/src/main/res/drawable-nodpi/marvel.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saulmm/Avengers/2b02f19d5a0ca3e36b5370f545036b30fd62a114/app/src/main/res/drawable-xhdpi/ic_action_favorite.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saulmm/Avengers/2b02f19d5a0ca3e36b5370f545036b30fd62a114/app/src/main/res/drawable-xxhdpi/ic_action_favorite.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saulmm/Avengers/2b02f19d5a0ca3e36b5370f545036b30fd62a114/app/src/main/res/drawable-xxxhdpi/ic_action_favorite.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_dark_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_avenger_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 35 | 36 | 44 | 45 | 58 | 59 | 60 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 83 | 84 | 85 | 86 | 94 | 95 | 102 | 103 | 111 | 112 | 123 | 124 | 130 | 131 | 140 | 141 | 142 | 153 | 154 | 160 | 161 | 170 | 171 | 182 | 183 | 189 | 190 | 199 | 200 | 201 | 212 | 213 | 219 | 220 | 229 | 230 | 231 | 232 | 233 | 234 | 240 | 241 | 247 | 248 | 257 | 258 | 270 | 271 | 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_avengers_list.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 22 | 23 | 27 | 28 | 34 | 35 | 41 | 42 | 49 | 50 | 59 | 60 | 61 | 62 | 63 | 64 | 71 | 72 | 80 | 81 | 90 | 91 | 92 | 93 | 102 | 103 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_character_collection.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_character.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 16 | 17 | 26 | 27 | 38 | 39 | 40 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_comic.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 22 | 23 | 30 | 31 | 38 | 39 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 19 | 20 | 29 | 30 |