├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── aars └── cloudtestingscreenshotter_lib.aar ├── app ├── .gitignore ├── build.gradle ├── dependencies.gradle ├── lint.xml ├── proguard-rules.pro └── src │ ├── androidTest │ ├── assets │ │ ├── getPokemon.json │ │ └── getPokemonDetails.json │ └── java │ │ └── io │ │ └── mvpstarter │ │ └── sample │ │ ├── DetailActivityTest.java │ │ ├── MainActivityTest.java │ │ ├── runner │ │ ├── RxAndroidJUnitRunner.java │ │ └── TestRunner.java │ │ └── util │ │ ├── ErrorTestUtil.java │ │ ├── RxIdlingResource.java │ │ └── RxIdlingScheduler.java │ ├── commonTest │ └── java │ │ └── io │ │ └── mvpstarter │ │ └── sample │ │ └── common │ │ ├── TestComponentRule.java │ │ ├── TestDataFactory.java │ │ └── injection │ │ ├── component │ │ └── TestComponent.java │ │ └── module │ │ └── ApplicationTestModule.java │ ├── debug │ ├── AndroidManifest.xml │ └── res │ │ └── values │ │ └── google_maps_api.xml │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── mvpstarter │ │ │ └── sample │ │ │ ├── Constants.java │ │ │ ├── MvpStarterApplication.java │ │ │ ├── data │ │ │ ├── DataManager.java │ │ │ ├── local │ │ │ │ ├── DbManager.java │ │ │ │ └── PreferencesHelper.java │ │ │ ├── model │ │ │ │ └── response │ │ │ │ │ ├── NamedResource.java │ │ │ │ │ ├── Pokemon.java │ │ │ │ │ ├── PokemonListResponse.java │ │ │ │ │ ├── Sprites.java │ │ │ │ │ └── Statistic.java │ │ │ └── remote │ │ │ │ └── PokemonService.java │ │ │ ├── features │ │ │ ├── base │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── BaseFragment.java │ │ │ │ ├── BasePresenter.java │ │ │ │ ├── MvpView.java │ │ │ │ └── Presenter.java │ │ │ ├── common │ │ │ │ └── ErrorView.java │ │ │ ├── detail │ │ │ │ ├── DetailActivity.java │ │ │ │ ├── DetailMvpView.java │ │ │ │ ├── DetailPresenter.java │ │ │ │ ├── MapsSampleActivity.java │ │ │ │ └── widget │ │ │ │ │ └── StatisticView.java │ │ │ └── main │ │ │ │ ├── MainActivity.java │ │ │ │ ├── MainMvpView.java │ │ │ │ ├── MainPresenter.java │ │ │ │ └── PokemonAdapter.java │ │ │ ├── injection │ │ │ ├── ActivityContext.java │ │ │ ├── ApplicationContext.java │ │ │ ├── ConfigPersistent.java │ │ │ ├── PerActivity.java │ │ │ ├── PerFragment.java │ │ │ ├── component │ │ │ │ ├── ActivityComponent.java │ │ │ │ ├── AppComponent.java │ │ │ │ ├── ConfigPersistentComponent.java │ │ │ │ └── FragmentComponent.java │ │ │ └── module │ │ │ │ ├── ActivityModule.java │ │ │ │ ├── ApiModule.java │ │ │ │ ├── AppModule.java │ │ │ │ ├── FragmentModule.java │ │ │ │ └── NetworkModule.java │ │ │ └── util │ │ │ ├── NetworkUtil.java │ │ │ ├── ViewUtil.java │ │ │ └── rx │ │ │ └── scheduler │ │ │ ├── BaseScheduler.java │ │ │ ├── ComputationMainScheduler.java │ │ │ ├── IoMainScheduler.java │ │ │ ├── NewThreadMainScheduler.java │ │ │ ├── SchedulerUtils.java │ │ │ ├── SingleMainScheduler.java │ │ │ └── TrampolineMainScheduler.java │ └── res │ │ ├── layout │ │ ├── activity_detail.xml │ │ ├── activity_main.xml │ │ ├── activity_maps_sample.xml │ │ ├── item_pokemon.xml │ │ ├── view_error.xml │ │ └── view_statistic.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── release │ └── res │ │ └── values │ │ └── google_maps_api.xml │ └── test │ ├── java │ └── io │ │ └── mvpstarter │ │ └── sample │ │ ├── DataManagerTest.java │ │ ├── DetailPresenterTest.java │ │ ├── MainPresenterTest.java │ │ └── util │ │ ├── DefaultConfig.java │ │ └── RxSchedulersOverrideRule.java │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── build.gradle ├── circle.yml ├── config ├── jacoco.gradle └── quality │ ├── checkstyle │ └── checkstyle-config.xml │ ├── findbugs │ └── android-exclude-filter.xml │ ├── pmd │ └── pmd-ruleset.xml │ └── quality.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts ├── ci.sh └── remoteTesting.sh └── settings.gradle /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | /build 6 | .idea/ 7 | *iml 8 | *.iml 9 | */build 10 | fastlane 11 | .tasks.cache.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: android 4 | jdk: oraclejdk8 5 | 6 | android: 7 | components: 8 | - platform-tools 9 | - tools # to get the new `repository-11.xml` 10 | - tools # see https://github.com/travis-ci/travis-ci/issues/6040#issuecomment-219367943) 11 | - build-tools-27.0.0 12 | - android-27 13 | - extra-android-m2repository 14 | - extra-google-m2repository 15 | - extra-android-support 16 | - extra-google-google_play_services 17 | 18 | before_script: 19 | - chmod +x gradlew 20 | 21 | script: "./gradlew testDebugUnitTest -PdisablePreDex" 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | ## Introduction 4 | 5 | First, thank you for considering contributing to android-starter! It's people like you that make the open source community such a great community! 😊 6 | 7 | We welcome any type of contribution, not only code. You can help with 8 | - **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) 9 | - **Marketing**: writing blog posts, howto's, printing stickers, ... 10 | - **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... 11 | - **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. 12 | - **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/android-starter). 13 | 14 | ## Your First Contribution 15 | 16 | Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 17 | 18 | ## Submitting code 19 | 20 | Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests. 21 | 22 | ## Code review process 23 | 24 | The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. 25 | It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? 26 | 27 | ## Financial contributions 28 | 29 | We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/android-starter). 30 | Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed. 31 | 32 | ## Questions 33 | 34 | If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!). 35 | You can also reach us at hello@android-starter.opencollective.com. 36 | 37 | ## Credits 38 | 39 | ### Contributors 40 | 41 | Thank you to all the people who have already contributed to android-starter! 42 | 43 | 44 | 45 | ### Backers 46 | 47 | Thank you to all our backers! [[Become a backer](https://opencollective.com/android-starter#backer)] 48 | 49 | 50 | 51 | 52 | ### Sponsors 53 | 54 | Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/android-starter#sponsor)) 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidMvpStarter [![Build Status](https://travis-ci.org/androidstarters/android-starter.svg?branch=master)](https://travis-ci.org/androidstarters/android-starter) 2 | 3 | [![Backers on Open Collective](https://opencollective.com/android-starter/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/android-starter/sponsors/badge.svg)](#sponsors) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Android%20MVP%20Starter-blue.svg?style=flat)](https://android-arsenal.com/details/3/5232) 4 | [![Join the chat at https://gitter.im/android-starter/Lobby](https://badges.gitter.im/android-starter/Lobby.svg)](https://gitter.im/android-starter/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | An MVP Boilerplate to save me having to create the same project over from scratch every time! :) 7 |

8 | 9 | 10 |

11 | 12 | ## This project uses: 13 | - [RxJava2](https://github.com/ReactiveX/RxJava) and [RxAndroid](https://github.com/ReactiveX/RxAndroid) 14 | - [Retrofit](http://square.github.io/retrofit/) / [OkHttp](http://square.github.io/okhttp/) 15 | - [Gson](https://github.com/google/gson) 16 | - [Dagger 2](http://google.github.io/dagger/) 17 | - [Butterknife](https://github.com/JakeWharton/butterknife) 18 | - [Google Play Services](https://developers.google.com/android/guides/overview) 19 | - [Timber](https://github.com/JakeWharton/timber) 20 | - [Glide 3](https://github.com/bumptech/glide) 21 | - [Stetho](http://facebook.github.io/stetho/) 22 | - [Espresso](https://google.github.io/android-testing-support-library/) for UI tests 23 | - [Robolectric](http://robolectric.org/) for framework specific unit tests 24 | - [Mockito](http://mockito.org/) 25 | - [Checkstyle](http://checkstyle.sourceforge.net/), [PMD](https://pmd.github.io/) and [Findbugs](http://findbugs.sourceforge.net/) for code analysis 26 | 27 | 28 | ## Create new project using yeoman [generator-android-mvp-starter](https://github.com/androidstarters/generator-android-mvp-starter) 29 | ```bash 30 | npm install -g yo 31 | npm install -g generator-android-mvp-starter 32 | mkdir NewApp && cd $_ 33 | yo android-mvp-starter 34 | ``` 35 | 36 | ## Building 37 | 38 | To build, install and run a debug version, run this from the root of the project: 39 | ```sh 40 | ./gradlew app:assembleDebug 41 | ``` 42 | 43 | ## Testing 44 | 45 | To run **unit** tests on your machine: 46 | 47 | ```sh 48 | ./gradlew test 49 | ``` 50 | 51 | To run **instrumentation** tests on connected devices: 52 | 53 | ```sh 54 | ./gradlew connectedAndroidTest 55 | ``` 56 | 57 | ## Code Analysis tools 58 | 59 | The following code analysis tools are set up on this project: 60 | 61 | * [PMD](https://pmd.github.io/) 62 | 63 | ```sh 64 | ./gradlew pmd 65 | ``` 66 | 67 | * [Findbugs](http://findbugs.sourceforge.net/) 68 | 69 | ```sh 70 | ./gradlew findbugs 71 | ``` 72 | 73 | * [Checkstyle](http://checkstyle.sourceforge.net/) 74 | 75 | ```sh 76 | ./gradlew checkstyle 77 | ``` 78 | 79 | ## The check task 80 | 81 | To ensure that your code is valid and stable use check: 82 | 83 | ```sh 84 | ./gradlew check 85 | ``` 86 | 87 | ## Jacoco Reports 88 | 89 | #### Generate Jacoco coverage reports for the Debug build. Only unit tests. 90 | 91 | ```sh 92 | app:testDebugUnitTestCoverage 93 | ``` 94 | 95 | #### Generate Jacoco coverage reports for the Release build. Only unit tests. 96 | 97 | ```sh 98 | app:testReleaseUnitTestCoverage 99 | ``` 100 | 101 | #### Generate Jacoco coverage reports for the Debug build. Both unit and espresso tests. 102 | 103 | ```sh 104 | app:unitAndEspressoDebugTestCoverage 105 | ``` 106 | 107 | #### Generate Jacoco coverage reports for the Release build. Both unit and espresso tests. 108 | 109 | ```sh 110 | app:unitAndEspressoReleaseTestCoverage 111 | ``` 112 | 113 | ### Created & Maintained By 114 | [Ravindra Kumar](https://github.com/ravidsrk) ([@ravidsrk](https://www.twitter.com/ravidsrk)) 115 | 116 | > If you found this repo helpful or you learned something from the source code and want to thank me, consider [buying me a cup of](https://www.paypal.me/ravidsrk) :coffee: 117 | 118 | ## Contributors 119 | 120 | This project exists thanks to all the people who contribute. [[Contribute]](CONTRIBUTING.md). 121 | 122 | 123 | 124 | ## Backers 125 | 126 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/android-starter#backer)] 127 | 128 | 129 | 130 | 131 | ## Sponsors 132 | 133 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/android-starter#sponsor)] 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | ## License 149 | ``` 150 | MIT License 151 | 152 | Copyright (c) 2017 Ravindra Kumar 153 | 154 | Permission is hereby granted, free of charge, to any person obtaining a copy 155 | of this software and associated documentation files (the "Software"), to deal 156 | in the Software without restriction, including without limitation the rights 157 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 158 | copies of the Software, and to permit persons to whom the Software is 159 | furnished to do so, subject to the following conditions: 160 | 161 | The above copyright notice and this permission notice shall be included in all 162 | copies or substantial portions of the Software. 163 | 164 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 165 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 166 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 167 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 168 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 169 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 170 | SOFTWARE. 171 | ``` 172 | -------------------------------------------------------------------------------- /aars/cloudtestingscreenshotter_lib.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/androidstarters/android-starter/a77488ccb337f3d08737259a2d17349fe783ac62/aars/cloudtestingscreenshotter_lib.aar -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml 4 | .idea -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply from: '../config/quality/quality.gradle' 3 | apply from: '../config/jacoco.gradle' 4 | apply plugin: 'com.github.ben-manes.versions' 5 | //apply plugin: 'io.fabric' 6 | 7 | android { 8 | compileSdkVersion 27 9 | buildToolsVersion '27.0.0' 10 | 11 | dexOptions { 12 | maxProcessCount 4 13 | preDexLibraries false 14 | javaMaxHeapSize "8g" 15 | } 16 | 17 | defaultConfig { 18 | defaultPublishConfig 'debug' 19 | 20 | applicationId "io.mvpstarter.sample" 21 | minSdkVersion 19 22 | targetSdkVersion 27 23 | testInstrumentationRunner "io.mvpstarter.sample.runner.RxAndroidJUnitRunner" 24 | versionCode 1000 25 | // Major -> Millions, Minor -> Thousands, Bugfix -> Hundreds. E.g 1.3.72 == 1,003,072 26 | versionName '0.1.0' 27 | 28 | buildConfigField("String", "POKEAPI_API_URL", 29 | "\"${PokeapiApiUrl}\"") 30 | } 31 | buildTypes { 32 | release { 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | } 36 | } 37 | 38 | sourceSets { 39 | def commonTestDir = 'src/commonTest/java' 40 | test { 41 | java.srcDir commonTestDir 42 | } 43 | androidTest { 44 | java.srcDir commonTestDir 45 | } 46 | } 47 | 48 | lintOptions { 49 | textOutput "stdout" 50 | textReport true 51 | checkAllWarnings true 52 | warningsAsErrors true 53 | showAll true 54 | explainIssues true 55 | abortOnError true 56 | ignoreWarnings false 57 | checkReleaseBuilds true 58 | htmlReport true 59 | xmlReport true 60 | lintConfig file("$projectDir/lint.xml") 61 | } 62 | 63 | packagingOptions { 64 | exclude 'META-INF/services/javax.annotation.processing.Processor' 65 | exclude 'LICENSE.txt' 66 | exclude 'META-INF/license/LICENSE.base64.txt' 67 | exclude 'META-INF/NOTICE.txt' 68 | exclude 'META-INF/LICENSE.txt' 69 | exclude 'META-INF/rxjava.properties' 70 | } 71 | 72 | compileOptions { 73 | sourceCompatibility JavaVersion.VERSION_1_8 74 | targetCompatibility JavaVersion.VERSION_1_8 75 | } 76 | 77 | testOptions { 78 | animationsDisabled = true 79 | unitTests { 80 | includeAndroidResources = true 81 | returnDefaultValues = true 82 | all { 83 | jacoco { 84 | includeNoLocationClasses true 85 | } 86 | systemProperty 'robolectric.logging.enabled', 'true' 87 | systemProperty 'robolectric.logging', 'stdout' 88 | // Print test details in the terminal (or CI). 89 | testLogging { 90 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' // Log out test results to console 91 | exceptionFormat 'full' 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | configurations.all { 99 | resolutionStrategy { 100 | force 'com.squareup.okio:okio:1.11.0' 101 | force "com.squareup.okhttp3:okhttp:3.5.0" 102 | } 103 | } 104 | 105 | 106 | apply from: 'dependencies.gradle' 107 | 108 | dependencies { 109 | 110 | implementation supportLibs 111 | implementation googlePlayLibs 112 | implementation networkLibs 113 | implementation rxJavaLibs 114 | implementation otherLibs 115 | 116 | debugImplementation debugLibs 117 | releaseImplementation releaseLibs 118 | 119 | debugImplementation(sherlock) { 120 | transitive = true 121 | } 122 | 123 | // APT dependencies 124 | annotationProcessor annotationProcessorLibs 125 | testAnnotationProcessor daggerCompiler 126 | androidTestAnnotationProcessor daggerCompiler 127 | 128 | testImplementation unitTestLibs 129 | androidTestImplementation androidTestsLibs 130 | } -------------------------------------------------------------------------------- /app/dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | versions = [ 3 | support : "27.0.2", 4 | firebase : '11.8.0', 5 | okHttp : "3.9.1", 6 | retrofit : '2.3.0', 7 | dagger : '2.14.1', 8 | butterKnife : '8.8.1', 9 | rxJava : '2.1.5', 10 | rxJavaAndroid : '2.0.1', 11 | rxPermissions : '0.9.4@aar', 12 | sqlbrite : '1.1.2', 13 | traceur : '1.0.1', 14 | stetho : '1.5.0', 15 | timber : '4.5.1', 16 | glide : '4.4.0', 17 | chuck : '1.1.0', 18 | leakcanary : '1.5.4', 19 | sherlock : '1.0.4@aar', 20 | 21 | espresso : '3.0.1', 22 | testRunner : '1.0.1', 23 | hamcrest : '1.3', 24 | restMock : '0.2.2', 25 | mockito : '2.13.0', 26 | junit : '4.12', 27 | robolectric : '3.4.2', 28 | assertj : '1.2.0' 29 | 30 | ] 31 | 32 | mockito = [mockito: "org.mockito:mockito-core:$versions.mockito"] 33 | junit = [junit: "junit:junit:$versions.junit"] 34 | restMock = [restMock: "com.github.andrzejchm.RESTMock:android:$versions.restMock"] 35 | 36 | supportDeps = [ 37 | cardView : "com.android.support:cardview-v7:$versions.support", 38 | appcompatV7 : "com.android.support:appcompat-v7:$versions.support", 39 | design : "com.android.support:design:$versions.support", 40 | recyclerView : "com.android.support:recyclerview-v7:$versions.support", 41 | supportAnnotation: "com.android.support:support-annotations:$versions.support", 42 | gridLayout : "com.android.support:gridlayout-v7:$versions.support" 43 | ] 44 | 45 | rxJava = [ 46 | rxJava : "io.reactivex.rxjava2:rxjava:$versions.rxJava", 47 | rxAndroid : "io.reactivex.rxjava2:rxandroid:$versions.rxJavaAndroid", 48 | traceur : "com.tspoon.traceur:traceur:$versions.traceur", 49 | rxRermissions : "com.tbruyelle.rxpermissions:rxpermissions:$versions.rxPermissions" 50 | ] 51 | 52 | retrofit = [ 53 | retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit", 54 | rxAdapter : "com.squareup.retrofit2:adapter-rxjava2:$versions.retrofit", 55 | gsonConverter: "com.squareup.retrofit2:converter-gson:$versions.retrofit", 56 | sqlbrite : "com.squareup.sqlbrite:sqlbrite:$versions.sqlbrite" 57 | ] 58 | 59 | okHttp = [ 60 | logger: "com.squareup.okhttp3:logging-interceptor:$versions.okHttp", 61 | okhttp : "com.squareup.okhttp3:okhttp:$versions.okHttp", 62 | stetho : "com.facebook.stetho:stetho-okhttp3:$versions.stetho" 63 | ] 64 | 65 | googlePlay = [ 66 | maps : "com.google.android.gms:play-services-maps:$versions.firebase", 67 | crash : "com.google.firebase:firebase-crash:$versions.firebase", 68 | firebaseCore : "com.google.firebase:firebase-core:$versions.firebase", 69 | firebaseMessaging: "com.google.firebase:firebase-messaging:$versions.firebase" 70 | ] 71 | 72 | onlyDebug = [ 73 | chuck : "com.readystatesoftware.chuck:library:$versions.chuck" 74 | ] 75 | 76 | onlyRelease = [ 77 | chuckNoOp : "com.readystatesoftware.chuck:library-no-op:$versions.chuck", 78 | sherlockNoOp : "com.github.ajitsing:sherlock-no-op:$versions.sherlock" 79 | ] 80 | 81 | unitTest = [ 82 | harcrestCore : "org.hamcrest:hamcrest-core:$versions.hamcrest", 83 | harmcrestLib : "org.hamcrest:hamcrest-library:$versions.hamcrest", 84 | hamcrestIntegration : "org.hamcrest:hamcrest-integration:$versions.hamcrest", 85 | robolectric : "org.robolectric:robolectric:$versions.robolectric", 86 | supportAnnotation : "com.android.support:support-annotations:$versions.support", 87 | restMock : "com.github.andrzejchm.RESTMock:android:$versions.restMock", 88 | assertj : "com.squareup.assertj:assertj-android:$versions.assertj", 89 | ] + junit + mockito + restMock 90 | 91 | androidTests = [ 92 | mockitoAndroid : "org.mockito:mockito-android:$versions.mockito", 93 | espressoCore : "com.android.support.test.espresso:espresso-core:$versions.espresso", 94 | espressoContrib : "com.android.support.test.espresso:espresso-contrib:$versions.espresso", 95 | espressoIntents : "com.android.support.test.espresso:espresso-intents:$versions.espresso", 96 | espressoAcc : "com.android.support.test.espresso:espresso-accessibility:$versions.espresso", 97 | espressoIdle : "com.android.support.test.espresso:espresso-idling-resource:$versions.espresso", 98 | espressoConcurrent : "com.android.support.test.espresso.idling:idling-concurrent:$versions.espresso", 99 | espressoNetIdle : "com.android.support.test.espresso.idling:idling-net:$versions.espresso", 100 | testRunner : "com.android.support.test:runner:$versions.testRunner", 101 | testRules : "com.android.support.test:rules:$versions.testRunner" 102 | ] + mockito + restMock 103 | 104 | leakCannary = "com.squareup.leakcanary:leakcanary-android:$versions.leakcanary" 105 | timber = "com.jakewharton.timber:timber:$versions.timber" 106 | sherlock = "com.github.ajitsing:sherlock:$versions.sherlock" 107 | 108 | dagger = "com.google.dagger:dagger:$versions.dagger" 109 | daggerCompiler = "com.google.dagger:dagger-compiler:$versions.dagger" 110 | 111 | butterKnife = "com.jakewharton:butterknife:$versions.butterKnife" 112 | butterKnifeCompiler = "com.jakewharton:butterknife-compiler:$versions.butterKnife" 113 | 114 | glide = "com.github.bumptech.glide:glide:$versions.glide" 115 | glideCompiler = "com.github.bumptech.glide:compiler:$versions.glide" 116 | glideOkhttp = "com.github.bumptech.glide:okhttp3-integration:$versions.glide" 117 | 118 | googlePlayLibs = googlePlay.values() 119 | supportLibs = supportDeps.values() 120 | networkLibs = retrofit.values() + okHttp.values() 121 | rxJavaLibs = rxJava.values() 122 | otherLibs = [butterKnife, timber, dagger, glide, glideOkhttp] 123 | 124 | debugLibs = onlyDebug.values() + leakCannary 125 | releaseLibs = onlyRelease.values() + leakCannary 126 | annotationProcessorLibs = [butterKnifeCompiler, daggerCompiler, glideCompiler] 127 | 128 | unitTestLibs = unitTest.values() + leakCannary 129 | androidTestsLibs = androidTests.values() + supportLibs 130 | } 131 | -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | // Unused will be removed on release 9 | // Using the material icons provided from Google 10 | // We might want to index our app later 11 | // Butterknife, Okio and Realm 12 | // Annotation binding 13 | 14 | 15 | // CI issue with sdk.dir in local.properties 16 | -------------------------------------------------------------------------------- /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 /usr/local/Cellar/android-sdk/24.4.1/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/assets/getPokemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 811, 3 | "previous": null, 4 | "results": [ 5 | { 6 | "url": "http://pokeapi.co/api/v2/pokemon/1/", 7 | "name": "bulbasaur" 8 | }, 9 | { 10 | "url": "http://pokeapi.co/api/v2/pokemon/2/", 11 | "name": "ivysaur" 12 | }, 13 | { 14 | "url": "http://pokeapi.co/api/v2/pokemon/3/", 15 | "name": "venusaur" 16 | }, 17 | { 18 | "url": "http://pokeapi.co/api/v2/pokemon/4/", 19 | "name": "charmander" 20 | }, 21 | { 22 | "url": "http://pokeapi.co/api/v2/pokemon/5/", 23 | "name": "charmeleon" 24 | }, 25 | { 26 | "url": "http://pokeapi.co/api/v2/pokemon/6/", 27 | "name": "charizard" 28 | }, 29 | { 30 | "url": "http://pokeapi.co/api/v2/pokemon/7/", 31 | "name": "squirtle" 32 | }, 33 | { 34 | "url": "http://pokeapi.co/api/v2/pokemon/8/", 35 | "name": "wartortle" 36 | }, 37 | { 38 | "url": "http://pokeapi.co/api/v2/pokemon/9/", 39 | "name": "blastoise" 40 | }, 41 | { 42 | "url": "http://pokeapi.co/api/v2/pokemon/10/", 43 | "name": "caterpie" 44 | }, 45 | { 46 | "url": "http://pokeapi.co/api/v2/pokemon/11/", 47 | "name": "metapod" 48 | }, 49 | { 50 | "url": "http://pokeapi.co/api/v2/pokemon/12/", 51 | "name": "butterfree" 52 | }, 53 | { 54 | "url": "http://pokeapi.co/api/v2/pokemon/13/", 55 | "name": "weedle" 56 | }, 57 | { 58 | "url": "http://pokeapi.co/api/v2/pokemon/14/", 59 | "name": "kakuna" 60 | }, 61 | { 62 | "url": "http://pokeapi.co/api/v2/pokemon/15/", 63 | "name": "beedrill" 64 | }, 65 | { 66 | "url": "http://pokeapi.co/api/v2/pokemon/16/", 67 | "name": "pidgey" 68 | }, 69 | { 70 | "url": "http://pokeapi.co/api/v2/pokemon/17/", 71 | "name": "pidgeotto" 72 | }, 73 | { 74 | "url": "http://pokeapi.co/api/v2/pokemon/18/", 75 | "name": "pidgeot" 76 | }, 77 | { 78 | "url": "http://pokeapi.co/api/v2/pokemon/19/", 79 | "name": "rattata" 80 | }, 81 | { 82 | "url": "http://pokeapi.co/api/v2/pokemon/20/", 83 | "name": "raticate" 84 | }, 85 | { 86 | "url": "http://pokeapi.co/api/v2/pokemon/21/", 87 | "name": "spearow" 88 | }, 89 | { 90 | "url": "http://pokeapi.co/api/v2/pokemon/22/", 91 | "name": "fearow" 92 | }, 93 | { 94 | "url": "http://pokeapi.co/api/v2/pokemon/23/", 95 | "name": "ekans" 96 | }, 97 | { 98 | "url": "http://pokeapi.co/api/v2/pokemon/24/", 99 | "name": "arbok" 100 | }, 101 | { 102 | "url": "http://pokeapi.co/api/v2/pokemon/25/", 103 | "name": "pikachu" 104 | }, 105 | { 106 | "url": "http://pokeapi.co/api/v2/pokemon/26/", 107 | "name": "raichu" 108 | }, 109 | { 110 | "url": "http://pokeapi.co/api/v2/pokemon/27/", 111 | "name": "sandshrew" 112 | }, 113 | { 114 | "url": "http://pokeapi.co/api/v2/pokemon/28/", 115 | "name": "sandslash" 116 | }, 117 | { 118 | "url": "http://pokeapi.co/api/v2/pokemon/29/", 119 | "name": "nidoran-f" 120 | }, 121 | { 122 | "url": "http://pokeapi.co/api/v2/pokemon/30/", 123 | "name": "nidorina" 124 | }, 125 | { 126 | "url": "http://pokeapi.co/api/v2/pokemon/31/", 127 | "name": "nidoqueen" 128 | }, 129 | { 130 | "url": "http://pokeapi.co/api/v2/pokemon/32/", 131 | "name": "nidoran-m" 132 | }, 133 | { 134 | "url": "http://pokeapi.co/api/v2/pokemon/33/", 135 | "name": "nidorino" 136 | }, 137 | { 138 | "url": "http://pokeapi.co/api/v2/pokemon/34/", 139 | "name": "nidoking" 140 | }, 141 | { 142 | "url": "http://pokeapi.co/api/v2/pokemon/35/", 143 | "name": "clefairy" 144 | }, 145 | { 146 | "url": "http://pokeapi.co/api/v2/pokemon/36/", 147 | "name": "clefable" 148 | }, 149 | { 150 | "url": "http://pokeapi.co/api/v2/pokemon/37/", 151 | "name": "vulpix" 152 | }, 153 | { 154 | "url": "http://pokeapi.co/api/v2/pokemon/38/", 155 | "name": "ninetales" 156 | }, 157 | { 158 | "url": "http://pokeapi.co/api/v2/pokemon/39/", 159 | "name": "jigglypuff" 160 | }, 161 | { 162 | "url": "http://pokeapi.co/api/v2/pokemon/40/", 163 | "name": "wigglytuff" 164 | }, 165 | { 166 | "url": "http://pokeapi.co/api/v2/pokemon/41/", 167 | "name": "zubat" 168 | }, 169 | { 170 | "url": "http://pokeapi.co/api/v2/pokemon/42/", 171 | "name": "golbat" 172 | }, 173 | { 174 | "url": "http://pokeapi.co/api/v2/pokemon/43/", 175 | "name": "oddish" 176 | }, 177 | { 178 | "url": "http://pokeapi.co/api/v2/pokemon/44/", 179 | "name": "gloom" 180 | }, 181 | { 182 | "url": "http://pokeapi.co/api/v2/pokemon/45/", 183 | "name": "vileplume" 184 | }, 185 | { 186 | "url": "http://pokeapi.co/api/v2/pokemon/46/", 187 | "name": "paras" 188 | }, 189 | { 190 | "url": "http://pokeapi.co/api/v2/pokemon/47/", 191 | "name": "parasect" 192 | }, 193 | { 194 | "url": "http://pokeapi.co/api/v2/pokemon/48/", 195 | "name": "venonat" 196 | }, 197 | { 198 | "url": "http://pokeapi.co/api/v2/pokemon/49/", 199 | "name": "venomoth" 200 | }, 201 | { 202 | "url": "http://pokeapi.co/api/v2/pokemon/50/", 203 | "name": "diglett" 204 | }, 205 | { 206 | "url": "http://pokeapi.co/api/v2/pokemon/51/", 207 | "name": "dugtrio" 208 | }, 209 | { 210 | "url": "http://pokeapi.co/api/v2/pokemon/52/", 211 | "name": "meowth" 212 | }, 213 | { 214 | "url": "http://pokeapi.co/api/v2/pokemon/53/", 215 | "name": "persian" 216 | }, 217 | { 218 | "url": "http://pokeapi.co/api/v2/pokemon/54/", 219 | "name": "psyduck" 220 | }, 221 | { 222 | "url": "http://pokeapi.co/api/v2/pokemon/55/", 223 | "name": "golduck" 224 | }, 225 | { 226 | "url": "http://pokeapi.co/api/v2/pokemon/56/", 227 | "name": "mankey" 228 | }, 229 | { 230 | "url": "http://pokeapi.co/api/v2/pokemon/57/", 231 | "name": "primeape" 232 | }, 233 | { 234 | "url": "http://pokeapi.co/api/v2/pokemon/58/", 235 | "name": "growlithe" 236 | }, 237 | { 238 | "url": "http://pokeapi.co/api/v2/pokemon/59/", 239 | "name": "arcanine" 240 | }, 241 | { 242 | "url": "http://pokeapi.co/api/v2/pokemon/60/", 243 | "name": "poliwag" 244 | }, 245 | { 246 | "url": "http://pokeapi.co/api/v2/pokemon/61/", 247 | "name": "poliwhirl" 248 | }, 249 | { 250 | "url": "http://pokeapi.co/api/v2/pokemon/62/", 251 | "name": "poliwrath" 252 | }, 253 | { 254 | "url": "http://pokeapi.co/api/v2/pokemon/63/", 255 | "name": "abra" 256 | }, 257 | { 258 | "url": "http://pokeapi.co/api/v2/pokemon/64/", 259 | "name": "kadabra" 260 | }, 261 | { 262 | "url": "http://pokeapi.co/api/v2/pokemon/65/", 263 | "name": "alakazam" 264 | }, 265 | { 266 | "url": "http://pokeapi.co/api/v2/pokemon/66/", 267 | "name": "machop" 268 | }, 269 | { 270 | "url": "http://pokeapi.co/api/v2/pokemon/67/", 271 | "name": "machoke" 272 | }, 273 | { 274 | "url": "http://pokeapi.co/api/v2/pokemon/68/", 275 | "name": "machamp" 276 | }, 277 | { 278 | "url": "http://pokeapi.co/api/v2/pokemon/69/", 279 | "name": "bellsprout" 280 | }, 281 | { 282 | "url": "http://pokeapi.co/api/v2/pokemon/70/", 283 | "name": "weepinbell" 284 | }, 285 | { 286 | "url": "http://pokeapi.co/api/v2/pokemon/71/", 287 | "name": "victreebel" 288 | }, 289 | { 290 | "url": "http://pokeapi.co/api/v2/pokemon/72/", 291 | "name": "tentacool" 292 | }, 293 | { 294 | "url": "http://pokeapi.co/api/v2/pokemon/73/", 295 | "name": "tentacruel" 296 | }, 297 | { 298 | "url": "http://pokeapi.co/api/v2/pokemon/74/", 299 | "name": "geodude" 300 | }, 301 | { 302 | "url": "http://pokeapi.co/api/v2/pokemon/75/", 303 | "name": "graveler" 304 | }, 305 | { 306 | "url": "http://pokeapi.co/api/v2/pokemon/76/", 307 | "name": "golem" 308 | }, 309 | { 310 | "url": "http://pokeapi.co/api/v2/pokemon/77/", 311 | "name": "ponyta" 312 | }, 313 | { 314 | "url": "http://pokeapi.co/api/v2/pokemon/78/", 315 | "name": "rapidash" 316 | }, 317 | { 318 | "url": "http://pokeapi.co/api/v2/pokemon/79/", 319 | "name": "slowpoke" 320 | }, 321 | { 322 | "url": "http://pokeapi.co/api/v2/pokemon/80/", 323 | "name": "slowbro" 324 | }, 325 | { 326 | "url": "http://pokeapi.co/api/v2/pokemon/81/", 327 | "name": "magnemite" 328 | }, 329 | { 330 | "url": "http://pokeapi.co/api/v2/pokemon/82/", 331 | "name": "magneton" 332 | }, 333 | { 334 | "url": "http://pokeapi.co/api/v2/pokemon/83/", 335 | "name": "farfetchd" 336 | }, 337 | { 338 | "url": "http://pokeapi.co/api/v2/pokemon/84/", 339 | "name": "doduo" 340 | }, 341 | { 342 | "url": "http://pokeapi.co/api/v2/pokemon/85/", 343 | "name": "dodrio" 344 | }, 345 | { 346 | "url": "http://pokeapi.co/api/v2/pokemon/86/", 347 | "name": "seel" 348 | }, 349 | { 350 | "url": "http://pokeapi.co/api/v2/pokemon/87/", 351 | "name": "dewgong" 352 | }, 353 | { 354 | "url": "http://pokeapi.co/api/v2/pokemon/88/", 355 | "name": "grimer" 356 | }, 357 | { 358 | "url": "http://pokeapi.co/api/v2/pokemon/89/", 359 | "name": "muk" 360 | }, 361 | { 362 | "url": "http://pokeapi.co/api/v2/pokemon/90/", 363 | "name": "shellder" 364 | }, 365 | { 366 | "url": "http://pokeapi.co/api/v2/pokemon/91/", 367 | "name": "cloyster" 368 | }, 369 | { 370 | "url": "http://pokeapi.co/api/v2/pokemon/92/", 371 | "name": "gastly" 372 | }, 373 | { 374 | "url": "http://pokeapi.co/api/v2/pokemon/93/", 375 | "name": "haunter" 376 | }, 377 | { 378 | "url": "http://pokeapi.co/api/v2/pokemon/94/", 379 | "name": "gengar" 380 | }, 381 | { 382 | "url": "http://pokeapi.co/api/v2/pokemon/95/", 383 | "name": "onix" 384 | }, 385 | { 386 | "url": "http://pokeapi.co/api/v2/pokemon/96/", 387 | "name": "drowzee" 388 | }, 389 | { 390 | "url": "http://pokeapi.co/api/v2/pokemon/97/", 391 | "name": "hypno" 392 | }, 393 | { 394 | "url": "http://pokeapi.co/api/v2/pokemon/98/", 395 | "name": "krabby" 396 | }, 397 | { 398 | "url": "http://pokeapi.co/api/v2/pokemon/99/", 399 | "name": "kingler" 400 | }, 401 | { 402 | "url": "http://pokeapi.co/api/v2/pokemon/100/", 403 | "name": "voltorb" 404 | }, 405 | { 406 | "url": "http://pokeapi.co/api/v2/pokemon/101/", 407 | "name": "electrode" 408 | }, 409 | { 410 | "url": "http://pokeapi.co/api/v2/pokemon/102/", 411 | "name": "exeggcute" 412 | }, 413 | { 414 | "url": "http://pokeapi.co/api/v2/pokemon/103/", 415 | "name": "exeggutor" 416 | }, 417 | { 418 | "url": "http://pokeapi.co/api/v2/pokemon/104/", 419 | "name": "cubone" 420 | }, 421 | { 422 | "url": "http://pokeapi.co/api/v2/pokemon/105/", 423 | "name": "marowak" 424 | }, 425 | { 426 | "url": "http://pokeapi.co/api/v2/pokemon/106/", 427 | "name": "hitmonlee" 428 | }, 429 | { 430 | "url": "http://pokeapi.co/api/v2/pokemon/107/", 431 | "name": "hitmonchan" 432 | }, 433 | { 434 | "url": "http://pokeapi.co/api/v2/pokemon/108/", 435 | "name": "lickitung" 436 | }, 437 | { 438 | "url": "http://pokeapi.co/api/v2/pokemon/109/", 439 | "name": "koffing" 440 | }, 441 | { 442 | "url": "http://pokeapi.co/api/v2/pokemon/110/", 443 | "name": "weezing" 444 | }, 445 | { 446 | "url": "http://pokeapi.co/api/v2/pokemon/111/", 447 | "name": "rhyhorn" 448 | }, 449 | { 450 | "url": "http://pokeapi.co/api/v2/pokemon/112/", 451 | "name": "rhydon" 452 | }, 453 | { 454 | "url": "http://pokeapi.co/api/v2/pokemon/113/", 455 | "name": "chansey" 456 | }, 457 | { 458 | "url": "http://pokeapi.co/api/v2/pokemon/114/", 459 | "name": "tangela" 460 | }, 461 | { 462 | "url": "http://pokeapi.co/api/v2/pokemon/115/", 463 | "name": "kangaskhan" 464 | }, 465 | { 466 | "url": "http://pokeapi.co/api/v2/pokemon/116/", 467 | "name": "horsea" 468 | }, 469 | { 470 | "url": "http://pokeapi.co/api/v2/pokemon/117/", 471 | "name": "seadra" 472 | }, 473 | { 474 | "url": "http://pokeapi.co/api/v2/pokemon/118/", 475 | "name": "goldeen" 476 | }, 477 | { 478 | "url": "http://pokeapi.co/api/v2/pokemon/119/", 479 | "name": "seaking" 480 | }, 481 | { 482 | "url": "http://pokeapi.co/api/v2/pokemon/120/", 483 | "name": "staryu" 484 | }, 485 | { 486 | "url": "http://pokeapi.co/api/v2/pokemon/121/", 487 | "name": "starmie" 488 | }, 489 | { 490 | "url": "http://pokeapi.co/api/v2/pokemon/122/", 491 | "name": "mr-mime" 492 | }, 493 | { 494 | "url": "http://pokeapi.co/api/v2/pokemon/123/", 495 | "name": "scyther" 496 | }, 497 | { 498 | "url": "http://pokeapi.co/api/v2/pokemon/124/", 499 | "name": "jynx" 500 | }, 501 | { 502 | "url": "http://pokeapi.co/api/v2/pokemon/125/", 503 | "name": "electabuzz" 504 | }, 505 | { 506 | "url": "http://pokeapi.co/api/v2/pokemon/126/", 507 | "name": "magmar" 508 | }, 509 | { 510 | "url": "http://pokeapi.co/api/v2/pokemon/127/", 511 | "name": "pinsir" 512 | }, 513 | { 514 | "url": "http://pokeapi.co/api/v2/pokemon/128/", 515 | "name": "tauros" 516 | }, 517 | { 518 | "url": "http://pokeapi.co/api/v2/pokemon/129/", 519 | "name": "magikarp" 520 | }, 521 | { 522 | "url": "http://pokeapi.co/api/v2/pokemon/130/", 523 | "name": "gyarados" 524 | }, 525 | { 526 | "url": "http://pokeapi.co/api/v2/pokemon/131/", 527 | "name": "lapras" 528 | }, 529 | { 530 | "url": "http://pokeapi.co/api/v2/pokemon/132/", 531 | "name": "ditto" 532 | }, 533 | { 534 | "url": "http://pokeapi.co/api/v2/pokemon/133/", 535 | "name": "eevee" 536 | }, 537 | { 538 | "url": "http://pokeapi.co/api/v2/pokemon/134/", 539 | "name": "vaporeon" 540 | }, 541 | { 542 | "url": "http://pokeapi.co/api/v2/pokemon/135/", 543 | "name": "jolteon" 544 | }, 545 | { 546 | "url": "http://pokeapi.co/api/v2/pokemon/136/", 547 | "name": "flareon" 548 | }, 549 | { 550 | "url": "http://pokeapi.co/api/v2/pokemon/137/", 551 | "name": "porygon" 552 | }, 553 | { 554 | "url": "http://pokeapi.co/api/v2/pokemon/138/", 555 | "name": "omanyte" 556 | }, 557 | { 558 | "url": "http://pokeapi.co/api/v2/pokemon/139/", 559 | "name": "omastar" 560 | }, 561 | { 562 | "url": "http://pokeapi.co/api/v2/pokemon/140/", 563 | "name": "kabuto" 564 | }, 565 | { 566 | "url": "http://pokeapi.co/api/v2/pokemon/141/", 567 | "name": "kabutops" 568 | }, 569 | { 570 | "url": "http://pokeapi.co/api/v2/pokemon/142/", 571 | "name": "aerodactyl" 572 | }, 573 | { 574 | "url": "http://pokeapi.co/api/v2/pokemon/143/", 575 | "name": "snorlax" 576 | }, 577 | { 578 | "url": "http://pokeapi.co/api/v2/pokemon/144/", 579 | "name": "articuno" 580 | }, 581 | { 582 | "url": "http://pokeapi.co/api/v2/pokemon/145/", 583 | "name": "zapdos" 584 | }, 585 | { 586 | "url": "http://pokeapi.co/api/v2/pokemon/146/", 587 | "name": "moltres" 588 | }, 589 | { 590 | "url": "http://pokeapi.co/api/v2/pokemon/147/", 591 | "name": "dratini" 592 | }, 593 | { 594 | "url": "http://pokeapi.co/api/v2/pokemon/148/", 595 | "name": "dragonair" 596 | }, 597 | { 598 | "url": "http://pokeapi.co/api/v2/pokemon/149/", 599 | "name": "dragonite" 600 | }, 601 | { 602 | "url": "http://pokeapi.co/api/v2/pokemon/150/", 603 | "name": "mewtwo" 604 | } 605 | ], 606 | "next": "http://pokeapi.co/api/v2/pokemon/?limit=150&offset=150" 607 | } 608 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/mvpstarter/sample/DetailActivityTest.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample; 2 | 3 | import android.support.test.InstrumentationRegistry; 4 | import android.support.test.rule.ActivityTestRule; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.junit.rules.RuleChain; 10 | import org.junit.rules.TestRule; 11 | import org.junit.runner.RunWith; 12 | 13 | import io.mvpstarter.sample.common.TestComponentRule; 14 | import io.mvpstarter.sample.common.TestDataFactory; 15 | import io.mvpstarter.sample.data.model.response.Pokemon; 16 | import io.mvpstarter.sample.data.model.response.Statistic; 17 | import io.mvpstarter.sample.features.detail.DetailActivity; 18 | import io.mvpstarter.sample.util.ErrorTestUtil; 19 | import io.reactivex.Single; 20 | 21 | import static android.support.test.espresso.Espresso.onView; 22 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 23 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 24 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 25 | import static org.mockito.ArgumentMatchers.anyString; 26 | import static org.mockito.Mockito.when; 27 | 28 | @RunWith(AndroidJUnit4.class) 29 | public class DetailActivityTest { 30 | 31 | public final TestComponentRule component = 32 | new TestComponentRule(InstrumentationRegistry.getTargetContext()); 33 | 34 | public final ActivityTestRule detailActivityTestRule = 35 | new ActivityTestRule<>(DetailActivity.class, false, false); 36 | 37 | // TestComponentRule needs to go first to make sure the Dagger ApplicationTestComponent is set 38 | // in the Application before any Activity is launched. 39 | @Rule 40 | public TestRule chain = RuleChain.outerRule(component).around(detailActivityTestRule); 41 | 42 | @Test 43 | public void checkPokemonDisplays() { 44 | Pokemon pokemon = TestDataFactory.makePokemon("id"); 45 | stubDataManagerGetPokemon(Single.just(pokemon)); 46 | detailActivityTestRule.launchActivity( 47 | DetailActivity.getStartIntent(InstrumentationRegistry.getContext(), pokemon.name)); 48 | 49 | for (Statistic stat : pokemon.stats) { 50 | onView(withText(stat.stat.name)).check(matches(isDisplayed())); 51 | } 52 | } 53 | 54 | @Test 55 | public void checkErrorViewDisplays() { 56 | stubDataManagerGetPokemon(Single.error(new RuntimeException())); 57 | Pokemon pokemon = TestDataFactory.makePokemon("id"); 58 | detailActivityTestRule.launchActivity( 59 | DetailActivity.getStartIntent(InstrumentationRegistry.getContext(), pokemon.name)); 60 | ErrorTestUtil.checkErrorViewsDisplay(); 61 | } 62 | 63 | public void stubDataManagerGetPokemon(Single single) { 64 | when(component.getMockApiManager().getPokemon(anyString())).thenReturn(single); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/mvpstarter/sample/MainActivityTest.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample; 2 | 3 | import android.support.test.InstrumentationRegistry; 4 | import android.support.test.rule.ActivityTestRule; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.junit.rules.RuleChain; 10 | import org.junit.rules.TestRule; 11 | import org.junit.runner.RunWith; 12 | 13 | import java.util.List; 14 | 15 | import io.mvpstarter.sample.common.TestComponentRule; 16 | import io.mvpstarter.sample.common.TestDataFactory; 17 | import io.mvpstarter.sample.data.model.response.NamedResource; 18 | import io.mvpstarter.sample.data.model.response.Pokemon; 19 | import io.mvpstarter.sample.features.main.MainActivity; 20 | import io.mvpstarter.sample.util.ErrorTestUtil; 21 | import io.reactivex.Single; 22 | 23 | import static android.support.test.espresso.Espresso.onView; 24 | import static android.support.test.espresso.action.ViewActions.click; 25 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 26 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 27 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 28 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 29 | import static org.mockito.ArgumentMatchers.anyInt; 30 | import static org.mockito.ArgumentMatchers.anyString; 31 | import static org.mockito.Mockito.when; 32 | 33 | @RunWith(AndroidJUnit4.class) 34 | public class MainActivityTest { 35 | 36 | private final TestComponentRule componentRule = 37 | new TestComponentRule(InstrumentationRegistry.getTargetContext()); 38 | private final ActivityTestRule mainActivityTestRule = 39 | new ActivityTestRule<>(MainActivity.class, false, false); 40 | 41 | // TestComponentRule needs to go first to make sure the Dagger ApplicationTestComponent is set 42 | // in the Application before any Activity is launched. 43 | @Rule 44 | public TestRule chain = RuleChain.outerRule(componentRule).around(mainActivityTestRule); 45 | 46 | @Test 47 | public void checkPokemonsDisplay() { 48 | List namedResourceList = TestDataFactory.makeNamedResourceList(5); 49 | List pokemonList = TestDataFactory.makePokemonNameList(namedResourceList); 50 | stubDataManagerGetPokemonList(Single.just(pokemonList)); 51 | mainActivityTestRule.launchActivity(null); 52 | 53 | for (NamedResource pokemonName : namedResourceList) { 54 | onView(withText(pokemonName.name)).check(matches(isDisplayed())); 55 | } 56 | } 57 | 58 | @Test 59 | public void clickingPokemonLaunchesDetailActivity() { 60 | List namedResourceList = TestDataFactory.makeNamedResourceList(5); 61 | List pokemonList = TestDataFactory.makePokemonNameList(namedResourceList); 62 | stubDataManagerGetPokemonList(Single.just(pokemonList)); 63 | stubDataManagerGetPokemon(Single.just(TestDataFactory.makePokemon("id"))); 64 | mainActivityTestRule.launchActivity(null); 65 | 66 | onView(withText(pokemonList.get(0))).perform(click()); 67 | 68 | onView(withId(R.id.image_pokemon)).check(matches(isDisplayed())); 69 | } 70 | 71 | @Test 72 | public void checkErrorViewDisplays() { 73 | stubDataManagerGetPokemonList(Single.error(new RuntimeException())); 74 | mainActivityTestRule.launchActivity(null); 75 | ErrorTestUtil.checkErrorViewsDisplay(); 76 | } 77 | 78 | public void stubDataManagerGetPokemonList(Single> single) { 79 | when(componentRule.getMockApiManager().getPokemonList(anyInt())).thenReturn(single); 80 | } 81 | 82 | public void stubDataManagerGetPokemon(Single single) { 83 | when(componentRule.getMockApiManager().getPokemon(anyString())).thenReturn(single); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/mvpstarter/sample/runner/RxAndroidJUnitRunner.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.runner; 2 | 3 | import android.support.test.espresso.IdlingRegistry; 4 | import android.support.test.espresso.accessibility.AccessibilityChecks; 5 | import android.support.test.runner.AndroidJUnitRunner; 6 | import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; 7 | import android.support.test.runner.lifecycle.Stage; 8 | 9 | import io.mvpstarter.sample.util.RxIdlingScheduler; 10 | import io.reactivex.Scheduler; 11 | import io.reactivex.plugins.RxJavaPlugins; 12 | 13 | import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; 14 | import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; 15 | import static android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON; 16 | 17 | /** 18 | * Runner that registers a Espresso Indling resource that handles waiting for RxJava Observables to 19 | * finish. WARNING - Using this runner will block the tests if the application uses long-lived hot 20 | * Observables such us event buses, etc. 21 | */ 22 | public class RxAndroidJUnitRunner extends AndroidJUnitRunner { 23 | 24 | @Override 25 | public void onStart() { 26 | // enableAccessibilityChecks(); 27 | dismissLockAndTurnScreenOn(); 28 | monitorRxSchedulerForIdleness(); 29 | super.onStart(); 30 | } 31 | 32 | /** 33 | * Run some automated accessibility checks. See https://google.github.io/android-testing-support-library/docs/accesibility-checking 34 | */ 35 | private void enableAccessibilityChecks() { 36 | AccessibilityChecks.enable(); 37 | } 38 | 39 | private void dismissLockAndTurnScreenOn() { 40 | ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback((activity, stage) -> { 41 | if (stage == Stage.PRE_ON_CREATE) { 42 | activity.getWindow().addFlags(FLAG_DISMISS_KEYGUARD | FLAG_TURN_SCREEN_ON | FLAG_KEEP_SCREEN_ON); 43 | } 44 | }); 45 | } 46 | 47 | private void monitorRxSchedulerForIdleness() { 48 | RxJavaPlugins.setInitIoSchedulerHandler(schedulerCallable 49 | -> convertToIdlingScheduler(schedulerCallable.call())); 50 | } 51 | 52 | private static RxIdlingScheduler convertToIdlingScheduler(Scheduler scheduler) { 53 | RxIdlingScheduler rxIdlingResource = new RxIdlingScheduler(scheduler); 54 | IdlingRegistry.getInstance().register(rxIdlingResource.getCountingIdlingResource()); 55 | return rxIdlingResource; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/mvpstarter/sample/runner/TestRunner.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.runner; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import io.appflate.restmock.android.RESTMockTestRunner; 7 | import io.mvpstarter.sample.MvpStarterApplication; 8 | 9 | /** 10 | * Created by ravindra on 4/2/17. 11 | */ 12 | public class TestRunner extends RESTMockTestRunner { 13 | 14 | @Override 15 | public Application newApplication(ClassLoader cl, String className, Context context) 16 | throws InstantiationException, IllegalAccessException, ClassNotFoundException { 17 | return super.newApplication(cl, MvpStarterApplication.class.getName(), context); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/mvpstarter/sample/util/ErrorTestUtil.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util; 2 | 3 | import io.mvpstarter.sample.R; 4 | 5 | import static android.support.test.espresso.Espresso.onView; 6 | import static android.support.test.espresso.action.ViewActions.click; 7 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 8 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 9 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 10 | import static org.hamcrest.Matchers.allOf; 11 | 12 | public class ErrorTestUtil { 13 | 14 | public static void checkErrorViewsDisplay() { 15 | onView(allOf(withText(R.string.error_title), isDisplayed())).check(matches(isDisplayed())); 16 | onView(allOf(withText(R.string.error_message), isDisplayed())) 17 | .check(matches(isDisplayed())); 18 | } 19 | 20 | public static void checkClickingReloadShowsContentWithText(String expectedText) { 21 | onView(allOf(withText(R.string.error_reload), isDisplayed())).perform(click()); 22 | onView(withText(expectedText)).check(matches(isDisplayed())); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/mvpstarter/sample/util/RxIdlingResource.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util; 2 | 3 | import android.support.test.espresso.IdlingResource; 4 | 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | import timber.log.Timber; 8 | 9 | /** 10 | * Espresso Idling resource that handles waiting for RxJava Observables executions. This class must 11 | * be used with RxIdlingScheduler. Before registering this idling resource you must: 1. Create 12 | * an instance of RxIdlingScheduler by passing an instance of this class. 2. Register 13 | * RxIdlingScheduler with the RxJavaPlugins using registerObservableExecutionHook() 3. Register 14 | * this idle resource with Espresso using Espresso.registerIdlingResources() 15 | */ 16 | public class RxIdlingResource implements IdlingResource { 17 | 18 | private final AtomicInteger activeSubscriptionsCount = new AtomicInteger(0); 19 | private ResourceCallback resourceCallback; 20 | 21 | @Override 22 | public String getName() { 23 | return getClass().getSimpleName(); 24 | } 25 | 26 | @Override 27 | public boolean isIdleNow() { 28 | return activeSubscriptionsCount.get() <= 0; 29 | } 30 | 31 | @Override 32 | public void registerIdleTransitionCallback(ResourceCallback callback) { 33 | resourceCallback = callback; 34 | } 35 | 36 | public void incrementActiveSubscriptionsCount() { 37 | int count = activeSubscriptionsCount.incrementAndGet(); 38 | Timber.i("Active subscriptions count increased to %d", count); 39 | } 40 | 41 | public void decrementActiveSubscriptionsCount() { 42 | int count = activeSubscriptionsCount.decrementAndGet(); 43 | Timber.i("Active subscriptions count decreased to %d", count); 44 | if (isIdleNow()) { 45 | Timber.i("There is no active subscriptions, transitioning to Idle"); 46 | resourceCallback.onTransitionToIdle(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/mvpstarter/sample/util/RxIdlingScheduler.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util; 2 | 3 | import android.support.test.espresso.idling.CountingIdlingResource; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import io.reactivex.Scheduler; 8 | import io.reactivex.disposables.Disposable; 9 | import rx.Observable; 10 | import rx.Subscription; 11 | import rx.plugins.RxJavaObservableExecutionHook; 12 | 13 | /** 14 | * RxJava Observable execution hook that handles updating the active subscription count for a given 15 | * Espresso RxIdlingResource. 16 | */ 17 | public class RxIdlingScheduler extends Scheduler { 18 | 19 | final CountingIdlingResource countingIdlingResource; 20 | private final Scheduler delegateScheduler; 21 | 22 | public RxIdlingScheduler(Scheduler scheduler) { 23 | this.delegateScheduler = scheduler; 24 | String resourceName = scheduler.getClass().getSimpleName() + scheduler.hashCode(); 25 | countingIdlingResource = new CountingIdlingResource(resourceName, true); 26 | } 27 | 28 | public CountingIdlingResource getCountingIdlingResource() { 29 | return countingIdlingResource; 30 | } 31 | 32 | @Override 33 | public Scheduler.Worker createWorker() { 34 | return new IdlingWorker(delegateScheduler.createWorker()); 35 | } 36 | 37 | private final class IdlingWorker extends Scheduler.Worker { 38 | 39 | private final Scheduler.Worker delegateWorker; 40 | private boolean recursive; 41 | 42 | private IdlingWorker(Scheduler.Worker worker) { 43 | this.delegateWorker = worker; 44 | } 45 | 46 | @Override 47 | public Disposable schedule(Runnable runnable) { 48 | return delegateWorker.schedule(recursive ? runnable : decorateAction(runnable)); 49 | } 50 | 51 | @Override 52 | public Disposable schedule(Runnable runnable, long delay, TimeUnit unit) { 53 | return delegateWorker.schedule(recursive ? runnable : decorateAction(runnable), delay, unit); 54 | } 55 | 56 | @Override 57 | public Disposable schedulePeriodically(Runnable runnable, long initialDelay, long period, TimeUnit unit) { 58 | recursive = true; 59 | return delegateWorker.schedulePeriodically(decorateAction(runnable), initialDelay, period, unit); 60 | } 61 | 62 | @Override 63 | public void dispose() { 64 | delegateWorker.dispose(); 65 | } 66 | 67 | @Override 68 | public boolean isDisposed() { 69 | return delegateWorker.isDisposed(); 70 | } 71 | 72 | private Runnable decorateAction(Runnable runnable) { 73 | countingIdlingResource.increment(); 74 | return () -> { 75 | try { 76 | runnable.run(); 77 | } finally { 78 | countingIdlingResource.decrement(); 79 | } 80 | }; 81 | } 82 | 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/commonTest/java/io/mvpstarter/sample/common/TestComponentRule.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.common; 2 | 3 | import android.content.Context; 4 | 5 | import org.junit.rules.TestRule; 6 | import org.junit.runner.Description; 7 | import org.junit.runners.model.Statement; 8 | 9 | import io.mvpstarter.sample.MvpStarterApplication; 10 | import io.mvpstarter.sample.common.injection.component.DaggerTestComponent; 11 | import io.mvpstarter.sample.common.injection.component.TestComponent; 12 | import io.mvpstarter.sample.common.injection.module.ApplicationTestModule; 13 | import io.mvpstarter.sample.data.DataManager; 14 | 15 | /** 16 | * Test rule that creates and sets a Dagger TestComponent into the application overriding the 17 | * existing application component. Use this rule in your test case in order for the app to use mock 18 | * dependencies. It also exposes some of the dependencies so they can be easily accessed from the 19 | * tests, e.g. to stub mocks etc. 20 | */ 21 | public class TestComponentRule implements TestRule { 22 | 23 | private final TestComponent testComponent; 24 | private final Context context; 25 | 26 | public TestComponentRule(Context context) { 27 | this.context = context; 28 | MvpStarterApplication application = MvpStarterApplication.get(context); 29 | testComponent = 30 | DaggerTestComponent.builder() 31 | .applicationTestModule(new ApplicationTestModule(application)) 32 | .build(); 33 | } 34 | 35 | public TestComponent getTestComponent() { 36 | return testComponent; 37 | } 38 | 39 | public Context getContext() { 40 | return context; 41 | } 42 | 43 | public DataManager getMockApiManager() { 44 | return testComponent.apiManager(); 45 | } 46 | 47 | @Override 48 | public Statement apply(final Statement base, Description description) { 49 | return new Statement() { 50 | @Override 51 | public void evaluate() throws Throwable { 52 | MvpStarterApplication application = MvpStarterApplication.get(context); 53 | application.setComponent(testComponent); 54 | base.evaluate(); 55 | application.setComponent(null); 56 | } 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/commonTest/java/io/mvpstarter/sample/common/TestDataFactory.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.common; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Random; 6 | import java.util.UUID; 7 | 8 | import io.mvpstarter.sample.data.model.response.NamedResource; 9 | import io.mvpstarter.sample.data.model.response.Pokemon; 10 | import io.mvpstarter.sample.data.model.response.Sprites; 11 | import io.mvpstarter.sample.data.model.response.Statistic; 12 | 13 | /** 14 | * Factory class that makes instances of data models with random field values. The aim of this class 15 | * is to help setting up test fixtures. 16 | */ 17 | public class TestDataFactory { 18 | 19 | private static final Random random = new Random(); 20 | 21 | public static String randomUuid() { 22 | return UUID.randomUUID().toString(); 23 | } 24 | 25 | public static Pokemon makePokemon(String id) { 26 | Pokemon pokemon = new Pokemon(); 27 | pokemon.id = id; 28 | pokemon.name = randomUuid() + id; 29 | pokemon.stats = makeStatisticList(3); 30 | pokemon.sprites = makeSprites(); 31 | return pokemon; 32 | } 33 | 34 | public static List makePokemonNamesList(int count) { 35 | List pokemonList = new ArrayList<>(); 36 | for (int i = 0; i < count; i++) { 37 | pokemonList.add(makePokemon(String.valueOf(i)).name); 38 | } 39 | return pokemonList; 40 | } 41 | 42 | public static List makePokemonNameList(List pokemonList) { 43 | List names = new ArrayList<>(); 44 | for (NamedResource pokemon : pokemonList) { 45 | names.add(pokemon.name); 46 | } 47 | return names; 48 | } 49 | 50 | public static Statistic makeStatistic() { 51 | Statistic statistic = new Statistic(); 52 | statistic.baseStat = random.nextInt(); 53 | statistic.stat = makeNamedResource(randomUuid()); 54 | return statistic; 55 | } 56 | 57 | public static List makeStatisticList(int count) { 58 | List statisticList = new ArrayList<>(); 59 | for (int i = 0; i < count; i++) { 60 | statisticList.add(makeStatistic()); 61 | } 62 | return statisticList; 63 | } 64 | 65 | public static Sprites makeSprites() { 66 | Sprites sprites = new Sprites(); 67 | sprites.frontDefault = randomUuid(); 68 | return sprites; 69 | } 70 | 71 | public static NamedResource makeNamedResource(String unique) { 72 | NamedResource namedResource = new NamedResource(); 73 | namedResource.name = randomUuid() + unique; 74 | namedResource.url = randomUuid(); 75 | return namedResource; 76 | } 77 | 78 | public static List makeNamedResourceList(int count) { 79 | List namedResourceList = new ArrayList<>(); 80 | for (int i = 0; i < count; i++) { 81 | namedResourceList.add(makeNamedResource(String.valueOf(i))); 82 | } 83 | return namedResourceList; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/commonTest/java/io/mvpstarter/sample/common/injection/component/TestComponent.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.common.injection.component; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import dagger.Component; 6 | import io.mvpstarter.sample.common.injection.module.ApplicationTestModule; 7 | import io.mvpstarter.sample.injection.component.AppComponent; 8 | 9 | @Singleton 10 | @Component(modules = ApplicationTestModule.class) 11 | public interface TestComponent extends AppComponent { 12 | } 13 | -------------------------------------------------------------------------------- /app/src/commonTest/java/io/mvpstarter/sample/common/injection/module/ApplicationTestModule.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.common.injection.module; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | import io.mvpstarter.sample.data.DataManager; 11 | import io.mvpstarter.sample.data.remote.PokemonService; 12 | import io.mvpstarter.sample.injection.ApplicationContext; 13 | 14 | import static org.mockito.Mockito.mock; 15 | 16 | /** 17 | * Provides application-level dependencies for an app running on a testing environment This allows 18 | * injecting mocks if necessary. 19 | */ 20 | @Module 21 | public class ApplicationTestModule { 22 | private final Application application; 23 | 24 | public ApplicationTestModule(Application application) { 25 | this.application = application; 26 | } 27 | 28 | @Provides 29 | @Singleton 30 | Application provideApplication() { 31 | return application; 32 | } 33 | 34 | @Provides 35 | @ApplicationContext 36 | Context provideContext() { 37 | return application; 38 | } 39 | 40 | /** 41 | * ********** MOCKS *********** 42 | */ 43 | @Provides 44 | @Singleton 45 | DataManager providesDataManager() { 46 | return mock(DataManager.class); 47 | } 48 | 49 | @Provides 50 | @Singleton 51 | PokemonService provideMvpBoilerplateService() { 52 | return mock(PokemonService.class); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/debug/res/values/google_maps_api.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | YOUR_KEY_HERE 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 43 | 46 | 47 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/Constants.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample; 2 | 3 | /** 4 | * Created by shivam on 29/5/17. 5 | */ 6 | public interface Constants { 7 | 8 | String PREF_FILE_NAME = "mvpstarter_pref_file"; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/MvpStarterApplication.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.facebook.stetho.Stetho; 7 | import com.singhajit.sherlock.core.Sherlock; 8 | import com.squareup.leakcanary.LeakCanary; 9 | import com.tspoon.traceur.Traceur; 10 | 11 | import io.mvpstarter.sample.injection.component.AppComponent; 12 | import io.mvpstarter.sample.injection.component.DaggerAppComponent; 13 | import io.mvpstarter.sample.injection.module.AppModule; 14 | import io.mvpstarter.sample.injection.module.NetworkModule; 15 | import timber.log.Timber; 16 | 17 | public class MvpStarterApplication extends Application { 18 | 19 | private AppComponent appComponent; 20 | 21 | public static MvpStarterApplication get(Context context) { 22 | return (MvpStarterApplication) context.getApplicationContext(); 23 | } 24 | 25 | @Override 26 | public void onCreate() { 27 | super.onCreate(); 28 | 29 | if (BuildConfig.DEBUG) { 30 | Timber.plant(new Timber.DebugTree()); 31 | Stetho.initializeWithDefaults(this); 32 | LeakCanary.install(this); 33 | Sherlock.init(this); 34 | Traceur.enableLogging(); 35 | } 36 | } 37 | 38 | public AppComponent getComponent() { 39 | if (appComponent == null) { 40 | appComponent = DaggerAppComponent.builder() 41 | .networkModule(new NetworkModule(this, BuildConfig.POKEAPI_API_URL)) 42 | .appModule(new AppModule(this)) 43 | .build(); 44 | } 45 | return appComponent; 46 | } 47 | 48 | // Needed to replace the component with a test specific one 49 | public void setComponent(AppComponent appComponent) { 50 | this.appComponent = appComponent; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/data/DataManager.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.data; 2 | 3 | import java.util.List; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Singleton; 7 | 8 | import io.mvpstarter.sample.data.model.response.Pokemon; 9 | import io.mvpstarter.sample.data.remote.PokemonService; 10 | import io.reactivex.Single; 11 | 12 | /** 13 | * Created by shivam on 29/5/17. 14 | */ 15 | @Singleton 16 | public class DataManager { 17 | 18 | private PokemonService pokemonService; 19 | 20 | @Inject 21 | public DataManager(PokemonService pokemonService) { 22 | this.pokemonService = pokemonService; 23 | } 24 | 25 | public Single> getPokemonList(int limit) { 26 | return pokemonService 27 | .getPokemonList(limit) 28 | .toObservable() 29 | .flatMapIterable(namedResources -> namedResources.results) 30 | .map(namedResource -> namedResource.name) 31 | .toList(); 32 | } 33 | 34 | public Single getPokemon(String name) { 35 | return pokemonService.getPokemon(name); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/data/local/DbManager.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.data.local; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Singleton; 5 | 6 | /** 7 | * Created by shivam on 29/5/17. 8 | */ 9 | 10 | // To be implemented with Realm 11 | 12 | @Singleton 13 | public class DbManager { 14 | 15 | @Inject 16 | public DbManager() { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/data/local/PreferencesHelper.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.data.local; 2 | 3 | import android.content.SharedPreferences; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.inject.Inject; 7 | import javax.inject.Singleton; 8 | 9 | @Singleton 10 | public class PreferencesHelper { 11 | 12 | private static final String PREF_FILE_NAME = "mvpstarter_pref_file"; 13 | 14 | private final SharedPreferences preferences; 15 | 16 | @Inject 17 | PreferencesHelper(SharedPreferences sharedPreferences) { 18 | preferences = sharedPreferences; 19 | } 20 | 21 | public void putString(@Nonnull String key, @Nonnull String value) { 22 | preferences.edit().putString(key, value).apply(); 23 | } 24 | 25 | public String getString(@Nonnull String key) { 26 | return preferences.getString(key, ""); 27 | } 28 | 29 | public void putBoolean(@Nonnull String key, @Nonnull boolean value) { 30 | preferences.edit().putBoolean(key, value).apply(); 31 | } 32 | 33 | public boolean getBoolean(@Nonnull String key) { 34 | return preferences.getBoolean(key, false); 35 | } 36 | 37 | public void putInt(@Nonnull String key, @Nonnull boolean value) { 38 | preferences.edit().putBoolean(key, value).apply(); 39 | } 40 | 41 | public int getInt(@Nonnull String key) { 42 | return preferences.getInt(key, -1); 43 | } 44 | 45 | public void clear() { 46 | preferences.edit().clear().apply(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/data/model/response/NamedResource.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.data.model.response; 2 | 3 | public class NamedResource { 4 | public String name; 5 | public String url; 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/data/model/response/Pokemon.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.data.model.response; 2 | 3 | import java.util.List; 4 | 5 | public class Pokemon { 6 | public String id; 7 | public String name; 8 | public Sprites sprites; 9 | public List stats; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/data/model/response/PokemonListResponse.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.data.model.response; 2 | 3 | import java.util.List; 4 | 5 | public class PokemonListResponse { 6 | public List results; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/data/model/response/Sprites.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.data.model.response; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class Sprites { 6 | @SerializedName("front_default") 7 | public String frontDefault; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/data/model/response/Statistic.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.data.model.response; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class Statistic { 6 | public NamedResource stat; 7 | 8 | @SerializedName("base_stat") 9 | public int baseStat; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/data/remote/PokemonService.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.data.remote; 2 | 3 | import io.mvpstarter.sample.data.model.response.Pokemon; 4 | import io.mvpstarter.sample.data.model.response.PokemonListResponse; 5 | import io.reactivex.Single; 6 | import retrofit2.http.GET; 7 | import retrofit2.http.Path; 8 | import retrofit2.http.Query; 9 | 10 | public interface PokemonService { 11 | 12 | @GET("pokemon") 13 | Single getPokemonList(@Query("limit") int limit); 14 | 15 | @GET("pokemon/{name}") 16 | Single getPokemon(@Path("name") String name); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/base/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.base; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.util.LongSparseArray; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.MenuItem; 7 | 8 | import java.util.concurrent.atomic.AtomicLong; 9 | 10 | import butterknife.ButterKnife; 11 | import io.mvpstarter.sample.MvpStarterApplication; 12 | import io.mvpstarter.sample.injection.component.ActivityComponent; 13 | import io.mvpstarter.sample.injection.component.ConfigPersistentComponent; 14 | import io.mvpstarter.sample.injection.component.DaggerConfigPersistentComponent; 15 | import io.mvpstarter.sample.injection.module.ActivityModule; 16 | import timber.log.Timber; 17 | 18 | /** 19 | * Abstract activity that every other Activity in this application must implement. It provides the 20 | * following functionality: - Handles creation of Dagger components and makes sure that instances of 21 | * ConfigPersistentComponent are kept across configuration changes. - Set up and handles a 22 | * GoogleApiClient instance that can be used to access the Google sign in api. - Handles signing out 23 | * when an authentication error event is received. 24 | */ 25 | public abstract class BaseActivity extends AppCompatActivity { 26 | 27 | private static final String KEY_ACTIVITY_ID = "KEY_ACTIVITY_ID"; 28 | private static final AtomicLong NEXT_ID = new AtomicLong(0); 29 | private static final LongSparseArray componentsArray = 30 | new LongSparseArray<>(); 31 | 32 | private long activityId; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(getLayout()); 38 | ButterKnife.bind(this); 39 | 40 | // Create the ActivityComponent and reuses cached ConfigPersistentComponent if this is 41 | // being called after a configuration change. 42 | activityId = 43 | savedInstanceState != null 44 | ? savedInstanceState.getLong(KEY_ACTIVITY_ID) 45 | : NEXT_ID.getAndIncrement(); 46 | ConfigPersistentComponent configPersistentComponent; 47 | if (componentsArray.get(activityId) == null) { 48 | Timber.i("Creating new ConfigPersistentComponent id=%d", activityId); 49 | configPersistentComponent = 50 | DaggerConfigPersistentComponent.builder() 51 | .appComponent(MvpStarterApplication.get(this).getComponent()) 52 | .build(); 53 | componentsArray.put(activityId, configPersistentComponent); 54 | } else { 55 | Timber.i("Reusing ConfigPersistentComponent id=%d", activityId); 56 | configPersistentComponent = componentsArray.get(activityId); 57 | } 58 | ActivityComponent activityComponent = 59 | configPersistentComponent.activityComponent(new ActivityModule(this)); 60 | inject(activityComponent); 61 | attachView(); 62 | } 63 | 64 | protected abstract int getLayout(); 65 | 66 | protected abstract void inject(ActivityComponent activityComponent); 67 | 68 | protected abstract void attachView(); 69 | 70 | protected abstract void detachPresenter(); 71 | 72 | @Override 73 | public boolean onOptionsItemSelected(MenuItem item) { 74 | switch (item.getItemId()) { 75 | case android.R.id.home: 76 | finish(); 77 | return true; 78 | default: 79 | return super.onOptionsItemSelected(item); 80 | } 81 | } 82 | 83 | @Override 84 | protected void onSaveInstanceState(Bundle outState) { 85 | super.onSaveInstanceState(outState); 86 | outState.putLong(KEY_ACTIVITY_ID, activityId); 87 | } 88 | 89 | @Override 90 | protected void onDestroy() { 91 | if (!isChangingConfigurations()) { 92 | Timber.i("Clearing ConfigPersistentComponent id=%d", activityId); 93 | componentsArray.remove(activityId); 94 | } 95 | detachPresenter(); 96 | super.onDestroy(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/base/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.base; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v4.util.LongSparseArray; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import java.util.concurrent.atomic.AtomicLong; 12 | 13 | import butterknife.ButterKnife; 14 | import io.mvpstarter.sample.MvpStarterApplication; 15 | import io.mvpstarter.sample.injection.component.ConfigPersistentComponent; 16 | import io.mvpstarter.sample.injection.component.DaggerConfigPersistentComponent; 17 | import io.mvpstarter.sample.injection.component.FragmentComponent; 18 | import io.mvpstarter.sample.injection.module.FragmentModule; 19 | import timber.log.Timber; 20 | 21 | /** 22 | * Abstract Fragment that every other Fragment in this application must implement. It handles 23 | * creation of Dagger components and makes sure that instances of ConfigPersistentComponent are kept 24 | * across configuration changes. 25 | */ 26 | public abstract class BaseFragment extends Fragment { 27 | 28 | private static final String KEY_FRAGMENT_ID = "KEY_FRAGMENT_ID"; 29 | private static final LongSparseArray componentsArray = 30 | new LongSparseArray<>(); 31 | private static final AtomicLong NEXT_ID = new AtomicLong(0); 32 | 33 | private long fragmentId; 34 | 35 | @Override 36 | public void onCreate(@Nullable Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | // Create the FragmentComponent and reuses cached ConfigPersistentComponent if this is 39 | // being called after a configuration change. 40 | fragmentId = 41 | savedInstanceState != null 42 | ? savedInstanceState.getLong(KEY_FRAGMENT_ID) 43 | : NEXT_ID.getAndIncrement(); 44 | ConfigPersistentComponent configPersistentComponent; 45 | if (componentsArray.get(fragmentId) == null) { 46 | Timber.i("Creating new ConfigPersistentComponent id=%d", fragmentId); 47 | configPersistentComponent = 48 | DaggerConfigPersistentComponent.builder() 49 | .appComponent(MvpStarterApplication.get(getActivity()).getComponent()) 50 | .build(); 51 | componentsArray.put(fragmentId, configPersistentComponent); 52 | } else { 53 | Timber.i("Reusing ConfigPersistentComponent id=%d", fragmentId); 54 | configPersistentComponent = componentsArray.get(fragmentId); 55 | } 56 | FragmentComponent fragmentComponent = 57 | configPersistentComponent.fragmentComponent(new FragmentModule(this)); 58 | inject(fragmentComponent); 59 | attachView(); 60 | } 61 | 62 | @Nullable 63 | @Override 64 | public View onCreateView( 65 | LayoutInflater inflater, 66 | @Nullable ViewGroup container, 67 | @Nullable Bundle savedInstanceState) { 68 | View view = inflater.inflate(getLayout(), container, false); 69 | ButterKnife.bind(this, view); 70 | return view; 71 | } 72 | 73 | protected abstract int getLayout(); 74 | 75 | protected abstract void inject(FragmentComponent fragmentComponent); 76 | 77 | protected abstract void attachView(); 78 | 79 | protected abstract void detachPresenter(); 80 | 81 | @Override 82 | public void onSaveInstanceState(Bundle outState) { 83 | super.onSaveInstanceState(outState); 84 | outState.putLong(KEY_FRAGMENT_ID, fragmentId); 85 | } 86 | 87 | @Override 88 | public void onDestroy() { 89 | if (!getActivity().isChangingConfigurations()) { 90 | Timber.i("Clearing ConfigPersistentComponent id=%d", fragmentId); 91 | componentsArray.remove(fragmentId); 92 | } 93 | detachPresenter(); 94 | super.onDestroy(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/base/BasePresenter.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.base; 2 | 3 | import io.reactivex.disposables.CompositeDisposable; 4 | import io.reactivex.disposables.Disposable; 5 | import rx.Observable; 6 | import rx.Single; 7 | 8 | /** 9 | * Base class that implements the Presenter interface and provides a base implementation for 10 | * attachView() and detachView(). It also handles keeping a reference to the mvpView that can be 11 | * accessed from the children classes by calling getView(). 12 | */ 13 | public class BasePresenter implements Presenter { 14 | 15 | private final CompositeDisposable compositeDisposable = new CompositeDisposable(); 16 | private T mvpView; 17 | 18 | @Override 19 | public void attachView(T mvpView) { 20 | this.mvpView = mvpView; 21 | } 22 | 23 | @Override 24 | public void detachView() { 25 | mvpView = null; 26 | if (!compositeDisposable.isDisposed()) { 27 | compositeDisposable.clear(); 28 | } 29 | } 30 | 31 | protected boolean isViewAttached() { 32 | return mvpView != null; 33 | } 34 | 35 | protected T getView() { 36 | return mvpView; 37 | } 38 | 39 | protected void checkViewAttached() { 40 | if (!isViewAttached()) throw new MvpViewNotAttachedException(); 41 | } 42 | 43 | public void addDisposable(Disposable disposable) { 44 | compositeDisposable.add(disposable); 45 | } 46 | 47 | private static class MvpViewNotAttachedException extends RuntimeException { 48 | MvpViewNotAttachedException() { 49 | super( 50 | "Please call Presenter.attachView(MvpView) before" 51 | + " requesting data to the Presenter"); 52 | } 53 | } 54 | 55 | /** 56 | * Encapsulate the result of an rx Observable. This model is meant to be used by the children 57 | * presenters to easily keep a reference to the latest loaded result so that it can be easily 58 | * emitted again when on configuration changes. 59 | */ 60 | protected static class DataResult { 61 | 62 | private T data; 63 | private Throwable error; 64 | 65 | public DataResult(T data) { 66 | this.data = data; 67 | } 68 | 69 | public DataResult(Throwable error) { 70 | this.error = error; 71 | } 72 | 73 | public Single toSingle() { 74 | if (error != null) { 75 | return Single.error(error); 76 | } 77 | return Single.just(data); 78 | } 79 | 80 | public Observable toObservable() { 81 | if (error != null) { 82 | return Observable.error(error); 83 | } 84 | return Observable.just(data); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/base/MvpView.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.base; 2 | 3 | /** 4 | * Base interface that any class that wants to act as a View in the MVP (Model View Presenter) 5 | * pattern must implement. Generally this interface will be extended by a more specific interface 6 | * that then usually will be implemented by an Activity or Fragment. 7 | */ 8 | public interface MvpView { 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/base/Presenter.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.base; 2 | 3 | /** 4 | * Every presenter in the app must either implement this interface or extend BasePresenter 5 | * indicating the MvpView type that wants to be attached with. 6 | */ 7 | public interface Presenter { 8 | 9 | void attachView(V mvpView); 10 | 11 | void detachView(); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/common/ErrorView.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.common; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.util.AttributeSet; 7 | import android.view.Gravity; 8 | import android.view.LayoutInflater; 9 | import android.widget.LinearLayout; 10 | 11 | import butterknife.ButterKnife; 12 | import butterknife.OnClick; 13 | import io.mvpstarter.sample.R; 14 | 15 | public class ErrorView extends LinearLayout { 16 | 17 | private ErrorListener errorListener; 18 | 19 | public ErrorView(Context context) { 20 | super(context); 21 | init(); 22 | } 23 | 24 | public ErrorView(Context context, AttributeSet attrs) { 25 | super(context, attrs); 26 | init(); 27 | } 28 | 29 | public ErrorView(Context context, AttributeSet attrs, int defStyleAttr) { 30 | super(context, attrs, defStyleAttr); 31 | init(); 32 | } 33 | 34 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 35 | public ErrorView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 36 | super(context, attrs, defStyleAttr, defStyleRes); 37 | init(); 38 | } 39 | 40 | private void init() { 41 | setOrientation(VERTICAL); 42 | setGravity(Gravity.CENTER); 43 | LayoutInflater.from(getContext()).inflate(R.layout.view_error, this); 44 | ButterKnife.bind(this); 45 | } 46 | 47 | @OnClick(R.id.button_reload) 48 | public void onReloadButtonClick() { 49 | if (errorListener != null) { 50 | errorListener.onReloadData(); 51 | } 52 | } 53 | 54 | public void setErrorListener(ErrorListener errorListener) { 55 | this.errorListener = errorListener; 56 | } 57 | 58 | public interface ErrorListener { 59 | void onReloadData(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/detail/DetailActivity.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.detail; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.app.ActionBar; 7 | import android.support.v7.widget.Toolbar; 8 | import android.view.View; 9 | import android.widget.ImageView; 10 | import android.widget.LinearLayout; 11 | import android.widget.ProgressBar; 12 | 13 | import com.bumptech.glide.Glide; 14 | 15 | import javax.inject.Inject; 16 | 17 | import butterknife.BindView; 18 | import io.mvpstarter.sample.R; 19 | import io.mvpstarter.sample.data.model.response.Pokemon; 20 | import io.mvpstarter.sample.data.model.response.Statistic; 21 | import io.mvpstarter.sample.features.base.BaseActivity; 22 | import io.mvpstarter.sample.features.common.ErrorView; 23 | import io.mvpstarter.sample.features.detail.widget.StatisticView; 24 | import io.mvpstarter.sample.injection.component.ActivityComponent; 25 | import timber.log.Timber; 26 | 27 | public class DetailActivity extends BaseActivity implements DetailMvpView, ErrorView.ErrorListener { 28 | 29 | public static final String EXTRA_POKEMON_NAME = "EXTRA_POKEMON_NAME"; 30 | 31 | @Inject 32 | DetailPresenter detailPresenter; 33 | 34 | @BindView(R.id.view_error) 35 | ErrorView errorView; 36 | 37 | @BindView(R.id.image_pokemon) 38 | ImageView pokemonImage; 39 | 40 | @BindView(R.id.progress) 41 | ProgressBar progress; 42 | 43 | @BindView(R.id.toolbar) 44 | Toolbar toolbar; 45 | 46 | @BindView(R.id.layout_stats) 47 | LinearLayout statLayout; 48 | 49 | @BindView(R.id.layout_pokemon) 50 | View pokemonLayout; 51 | 52 | private String pokemonName; 53 | 54 | public static Intent getStartIntent(Context context, String pokemonName) { 55 | Intent intent = new Intent(context, DetailActivity.class); 56 | intent.putExtra(EXTRA_POKEMON_NAME, pokemonName); 57 | return intent; 58 | } 59 | 60 | @Override 61 | protected void onCreate(Bundle savedInstanceState) { 62 | super.onCreate(savedInstanceState); 63 | 64 | pokemonName = getIntent().getStringExtra(EXTRA_POKEMON_NAME); 65 | if (pokemonName == null) { 66 | throw new IllegalArgumentException("Detail Activity requires a pokemon name@"); 67 | } 68 | 69 | setSupportActionBar(toolbar); 70 | ActionBar actionBar = getSupportActionBar(); 71 | if (actionBar != null) actionBar.setDisplayHomeAsUpEnabled(true); 72 | setTitle(pokemonName.substring(0, 1).toUpperCase() + pokemonName.substring(1)); 73 | 74 | errorView.setErrorListener(this); 75 | 76 | detailPresenter.getPokemon(pokemonName); 77 | } 78 | 79 | @Override 80 | public int getLayout() { 81 | return R.layout.activity_detail; 82 | } 83 | 84 | @Override 85 | protected void inject(ActivityComponent activityComponent) { 86 | activityComponent.inject(this); 87 | } 88 | 89 | @Override 90 | protected void attachView() { 91 | detailPresenter.attachView(this); 92 | } 93 | 94 | @Override 95 | protected void detachPresenter() { 96 | detailPresenter.detachView(); 97 | } 98 | 99 | @Override 100 | public void showPokemon(Pokemon pokemon) { 101 | if (pokemon.sprites != null && pokemon.sprites.frontDefault != null) { 102 | Glide.with(this).load(pokemon.sprites.frontDefault).into(pokemonImage); 103 | } 104 | pokemonLayout.setVisibility(View.VISIBLE); 105 | } 106 | 107 | @Override 108 | public void showStat(Statistic statistic) { 109 | StatisticView statisticView = new StatisticView(this); 110 | statisticView.setStat(statistic); 111 | statLayout.addView(statisticView); 112 | } 113 | 114 | @Override 115 | public void showProgress(boolean show) { 116 | errorView.setVisibility(View.GONE); 117 | progress.setVisibility(show ? View.VISIBLE : View.GONE); 118 | } 119 | 120 | @Override 121 | public void showError(Throwable error) { 122 | pokemonLayout.setVisibility(View.GONE); 123 | errorView.setVisibility(View.VISIBLE); 124 | Timber.e(error, "There was a problem retrieving the pokemon..."); 125 | } 126 | 127 | @Override 128 | public void onReloadData() { 129 | detailPresenter.getPokemon(pokemonName); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/detail/DetailMvpView.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.detail; 2 | 3 | import io.mvpstarter.sample.data.model.response.Pokemon; 4 | import io.mvpstarter.sample.data.model.response.Statistic; 5 | import io.mvpstarter.sample.features.base.MvpView; 6 | 7 | public interface DetailMvpView extends MvpView { 8 | 9 | void showPokemon(Pokemon pokemon); 10 | 11 | void showStat(Statistic statistic); 12 | 13 | void showProgress(boolean show); 14 | 15 | void showError(Throwable error); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/detail/DetailPresenter.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.detail; 2 | 3 | import javax.inject.Inject; 4 | 5 | import io.mvpstarter.sample.data.DataManager; 6 | import io.mvpstarter.sample.data.model.response.Statistic; 7 | import io.mvpstarter.sample.features.base.BasePresenter; 8 | import io.mvpstarter.sample.injection.ConfigPersistent; 9 | import io.mvpstarter.sample.util.rx.scheduler.SchedulerUtils; 10 | 11 | @ConfigPersistent 12 | public class DetailPresenter extends BasePresenter { 13 | 14 | private final DataManager dataManager; 15 | 16 | @Inject 17 | public DetailPresenter(DataManager dataManager) { 18 | this.dataManager = dataManager; 19 | } 20 | 21 | @Override 22 | public void attachView(DetailMvpView mvpView) { 23 | super.attachView(mvpView); 24 | } 25 | 26 | public void getPokemon(String name) { 27 | checkViewAttached(); 28 | getView().showProgress(true); 29 | dataManager 30 | .getPokemon(name) 31 | .compose(SchedulerUtils.ioToMain()) 32 | .subscribe( 33 | pokemon -> { 34 | // It should be always checked if MvpView (Fragment or Activity) is attached. 35 | // Calling showProgress() on a not-attached fragment will throw a NPE 36 | // It is possible to ask isAdded() in the fragment, but it's better to ask in the presenter 37 | getView().showProgress(false); 38 | getView().showPokemon(pokemon); 39 | for (Statistic statistic : pokemon.stats) { 40 | getView().showStat(statistic); 41 | } 42 | }, 43 | throwable -> { 44 | getView().showProgress(false); 45 | getView().showError(throwable); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/detail/MapsSampleActivity.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.detail; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.google.android.gms.maps.CameraUpdateFactory; 7 | import com.google.android.gms.maps.GoogleMap; 8 | import com.google.android.gms.maps.OnMapReadyCallback; 9 | import com.google.android.gms.maps.SupportMapFragment; 10 | import com.google.android.gms.maps.model.LatLng; 11 | import com.google.android.gms.maps.model.MarkerOptions; 12 | 13 | import io.mvpstarter.sample.R; 14 | 15 | public class MapsSampleActivity extends AppCompatActivity implements OnMapReadyCallback { 16 | 17 | private GoogleMap googleMap; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_maps_sample); 23 | // Obtain the SupportMapFragment and get notified when the map is ready to be used. 24 | SupportMapFragment mapFragment = 25 | (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map); 26 | mapFragment.getMapAsync(this); 27 | } 28 | 29 | /** 30 | * Manipulates the map once available. This callback is triggered when the map is ready to be 31 | * used. This is where we can add markers or lines, add listeners or move the camera. In this 32 | * case, we just add a marker near Sydney, Australia. If Google Play services is not installed 33 | * on the device, the user will be prompted to install it inside the SupportMapFragment. This 34 | * method will only be triggered once the user has installed Google Play services and returned 35 | * to the app. 36 | */ 37 | @Override 38 | public void onMapReady(GoogleMap googleMap) { 39 | this.googleMap = googleMap; 40 | 41 | // Add a marker in Sydney and move the camera 42 | LatLng sydney = new LatLng(-34, 151); 43 | googleMap.addMarker(new MarkerOptions().position(sydney).title("Marker in Sydney")); 44 | googleMap.moveCamera(CameraUpdateFactory.newLatLng(sydney)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/detail/widget/StatisticView.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.detail.widget; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.annotation.TargetApi; 5 | import android.content.Context; 6 | import android.os.Build; 7 | import android.util.AttributeSet; 8 | import android.view.LayoutInflater; 9 | import android.widget.ProgressBar; 10 | import android.widget.RelativeLayout; 11 | import android.widget.TextView; 12 | 13 | import butterknife.BindView; 14 | import butterknife.ButterKnife; 15 | import io.mvpstarter.sample.R; 16 | import io.mvpstarter.sample.data.model.response.Statistic; 17 | 18 | public class StatisticView extends RelativeLayout { 19 | 20 | @BindView(R.id.text_name) 21 | TextView nameText; 22 | 23 | @BindView(R.id.progress_stat) 24 | ProgressBar statProgress; 25 | 26 | public StatisticView(Context context) { 27 | super(context); 28 | init(); 29 | } 30 | 31 | public StatisticView(Context context, AttributeSet attrs) { 32 | super(context, attrs); 33 | init(); 34 | } 35 | 36 | public StatisticView(Context context, AttributeSet attrs, int defStyleAttr) { 37 | super(context, attrs, defStyleAttr); 38 | init(); 39 | } 40 | 41 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 42 | public StatisticView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 43 | super(context, attrs, defStyleAttr, defStyleRes); 44 | init(); 45 | } 46 | 47 | private void init() { 48 | LayoutInflater.from(getContext()).inflate(R.layout.view_statistic, this); 49 | ButterKnife.bind(this); 50 | } 51 | 52 | @SuppressLint("SetTextI18n") 53 | public void setStat(Statistic statistic) { 54 | nameText.setText( 55 | statistic.stat.name.substring(0, 1).toUpperCase() 56 | + statistic.stat.name.substring(1)); 57 | statProgress.setProgress(statistic.baseStat); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/main/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.main; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.widget.SwipeRefreshLayout; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.support.v7.widget.Toolbar; 8 | import android.view.View; 9 | import android.widget.ProgressBar; 10 | import android.widget.Toast; 11 | 12 | import java.util.List; 13 | 14 | import javax.inject.Inject; 15 | 16 | import butterknife.BindView; 17 | import io.mvpstarter.sample.R; 18 | import io.mvpstarter.sample.features.base.BaseActivity; 19 | import io.mvpstarter.sample.features.common.ErrorView; 20 | import io.mvpstarter.sample.features.detail.DetailActivity; 21 | import io.mvpstarter.sample.injection.component.ActivityComponent; 22 | import io.reactivex.disposables.Disposable; 23 | import timber.log.Timber; 24 | 25 | public class MainActivity extends BaseActivity implements MainMvpView, ErrorView.ErrorListener { 26 | 27 | private static final int POKEMON_COUNT = 20; 28 | 29 | @Inject 30 | PokemonAdapter pokemonAdapter; 31 | @Inject 32 | MainPresenter mainPresenter; 33 | 34 | @BindView(R.id.view_error) 35 | ErrorView errorView; 36 | 37 | @BindView(R.id.progress) 38 | ProgressBar progressBar; 39 | 40 | @BindView(R.id.recycler_pokemon) 41 | RecyclerView pokemonRecycler; 42 | 43 | @BindView(R.id.swipe_to_refresh) 44 | SwipeRefreshLayout swipeRefreshLayout; 45 | 46 | @BindView(R.id.toolbar) 47 | Toolbar toolbar; 48 | 49 | @Override 50 | protected void onCreate(Bundle savedInstanceState) { 51 | super.onCreate(savedInstanceState); 52 | 53 | setSupportActionBar(toolbar); 54 | 55 | swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.primary); 56 | swipeRefreshLayout.setColorSchemeResources(R.color.white); 57 | swipeRefreshLayout.setOnRefreshListener(() -> mainPresenter.getPokemon(POKEMON_COUNT)); 58 | 59 | pokemonRecycler.setLayoutManager(new LinearLayoutManager(this)); 60 | pokemonRecycler.setAdapter(pokemonAdapter); 61 | pokemonClicked(); 62 | errorView.setErrorListener(this); 63 | 64 | mainPresenter.getPokemon(POKEMON_COUNT); 65 | } 66 | 67 | private void pokemonClicked() { 68 | Disposable disposable = 69 | pokemonAdapter 70 | .getPokemonClick() 71 | .subscribe( 72 | pokemon -> 73 | startActivity(DetailActivity.getStartIntent(this, pokemon)), 74 | throwable -> { 75 | Timber.e(throwable, "Pokemon click failed"); 76 | Toast.makeText( 77 | this, 78 | R.string.error_something_bad_happened, 79 | Toast.LENGTH_LONG) 80 | .show(); 81 | }); 82 | mainPresenter.addDisposable(disposable); 83 | } 84 | 85 | @Override 86 | public int getLayout() { 87 | return R.layout.activity_main; 88 | } 89 | 90 | @Override 91 | protected void inject(ActivityComponent activityComponent) { 92 | activityComponent.inject(this); 93 | } 94 | 95 | @Override 96 | protected void attachView() { 97 | mainPresenter.attachView(this); 98 | } 99 | 100 | @Override 101 | protected void detachPresenter() { 102 | mainPresenter.detachView(); 103 | } 104 | 105 | @Override 106 | public void showPokemon(List pokemon) { 107 | pokemonAdapter.setPokemon(pokemon); 108 | pokemonRecycler.setVisibility(View.VISIBLE); 109 | swipeRefreshLayout.setVisibility(View.VISIBLE); 110 | } 111 | 112 | @Override 113 | public void showProgress(boolean show) { 114 | if (show) { 115 | if (pokemonRecycler.getVisibility() == View.VISIBLE 116 | && pokemonAdapter.getItemCount() > 0) { 117 | swipeRefreshLayout.setRefreshing(true); 118 | } else { 119 | progressBar.setVisibility(View.VISIBLE); 120 | 121 | pokemonRecycler.setVisibility(View.GONE); 122 | swipeRefreshLayout.setVisibility(View.GONE); 123 | } 124 | 125 | errorView.setVisibility(View.GONE); 126 | } else { 127 | swipeRefreshLayout.setRefreshing(false); 128 | progressBar.setVisibility(View.GONE); 129 | } 130 | } 131 | 132 | @Override 133 | public void showError(Throwable error) { 134 | pokemonRecycler.setVisibility(View.GONE); 135 | swipeRefreshLayout.setVisibility(View.GONE); 136 | errorView.setVisibility(View.VISIBLE); 137 | Timber.e(error, "There was an error retrieving the pokemon"); 138 | } 139 | 140 | @Override 141 | public void onReloadData() { 142 | mainPresenter.getPokemon(POKEMON_COUNT); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/main/MainMvpView.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.main; 2 | 3 | import java.util.List; 4 | 5 | import io.mvpstarter.sample.features.base.MvpView; 6 | 7 | public interface MainMvpView extends MvpView { 8 | 9 | void showPokemon(List pokemon); 10 | 11 | void showProgress(boolean show); 12 | 13 | void showError(Throwable error); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/main/MainPresenter.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.main; 2 | 3 | import javax.inject.Inject; 4 | 5 | import io.mvpstarter.sample.data.DataManager; 6 | import io.mvpstarter.sample.features.base.BasePresenter; 7 | import io.mvpstarter.sample.injection.ConfigPersistent; 8 | import io.mvpstarter.sample.util.rx.scheduler.SchedulerUtils; 9 | 10 | @ConfigPersistent 11 | public class MainPresenter extends BasePresenter { 12 | 13 | private final DataManager dataManager; 14 | 15 | @Inject 16 | public MainPresenter(DataManager dataManager) { 17 | this.dataManager = dataManager; 18 | } 19 | 20 | @Override 21 | public void attachView(MainMvpView mvpView) { 22 | super.attachView(mvpView); 23 | } 24 | 25 | public void getPokemon(int limit) { 26 | checkViewAttached(); 27 | getView().showProgress(true); 28 | dataManager 29 | .getPokemonList(limit) 30 | .compose(SchedulerUtils.ioToMain()) 31 | .subscribe( 32 | pokemons -> { 33 | getView().showProgress(false); 34 | getView().showPokemon(pokemons); 35 | }, 36 | throwable -> { 37 | getView().showProgress(false); 38 | getView().showError(throwable); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/features/main/PokemonAdapter.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.features.main; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.TextView; 8 | 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import javax.inject.Inject; 13 | 14 | import butterknife.BindView; 15 | import butterknife.ButterKnife; 16 | import io.mvpstarter.sample.R; 17 | import io.reactivex.Observable; 18 | import io.reactivex.subjects.PublishSubject; 19 | import io.reactivex.subjects.Subject; 20 | 21 | public class PokemonAdapter extends RecyclerView.Adapter { 22 | 23 | private List pokemonList; 24 | private Subject pokemonClickSubject; 25 | 26 | @Inject 27 | PokemonAdapter() { 28 | pokemonClickSubject = PublishSubject.create(); 29 | pokemonList = Collections.emptyList(); 30 | } 31 | 32 | public void setPokemon(List pokemon) { 33 | this.pokemonList = pokemon; 34 | notifyDataSetChanged(); 35 | } 36 | 37 | @Override 38 | public PokemonViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 39 | View view = 40 | LayoutInflater.from(parent.getContext()) 41 | .inflate(R.layout.item_pokemon, parent, false); 42 | return new PokemonViewHolder(view); 43 | } 44 | 45 | @Override 46 | public void onBindViewHolder(PokemonViewHolder holder, int position) { 47 | String pokemon = this.pokemonList.get(position); 48 | holder.onBind(pokemon); 49 | } 50 | 51 | @Override 52 | public int getItemCount() { 53 | return pokemonList.size(); 54 | } 55 | 56 | Observable getPokemonClick() { 57 | return pokemonClickSubject; 58 | } 59 | 60 | class PokemonViewHolder extends RecyclerView.ViewHolder { 61 | 62 | @BindView(R.id.text_name) 63 | TextView nameText; 64 | 65 | private String pokemon; 66 | 67 | PokemonViewHolder(View itemView) { 68 | super(itemView); 69 | ButterKnife.bind(this, itemView); 70 | itemView.setOnClickListener(v -> pokemonClickSubject.onNext(pokemon)); 71 | } 72 | 73 | void onBind(String pokemon) { 74 | this.pokemon = pokemon; 75 | nameText.setText( 76 | String.format( 77 | "%s%s", pokemon.substring(0, 1).toUpperCase(), pokemon.substring(1))); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/ActivityContext.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Qualifier; 7 | 8 | @Qualifier 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface ActivityContext { 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/ApplicationContext.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Qualifier; 7 | 8 | @Qualifier 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface ApplicationContext { 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/ConfigPersistent.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Scope; 7 | 8 | 9 | /** 10 | * A scoping annotation to permit dependencies conform to the life of the {@link 11 | * ConfigPersistentComponent} 12 | */ 13 | @Scope 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface ConfigPersistent { 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/PerActivity.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Scope; 7 | 8 | /** 9 | * A scoping annotation to permit objects whose lifetime should conform to the life of the Activity 10 | * to be memorised in the correct component. 11 | */ 12 | @Scope 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface PerActivity { 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/PerFragment.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Scope; 7 | 8 | /** 9 | * A scoping annotation to permit objects whose lifetime should conform to the life of the Fragment 10 | * to be memorised in the correct component. 11 | */ 12 | @Scope 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface PerFragment { 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/component/ActivityComponent.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection.component; 2 | 3 | import dagger.Subcomponent; 4 | import io.mvpstarter.sample.features.detail.DetailActivity; 5 | import io.mvpstarter.sample.features.main.MainActivity; 6 | import io.mvpstarter.sample.injection.PerActivity; 7 | import io.mvpstarter.sample.injection.module.ActivityModule; 8 | 9 | @PerActivity 10 | @Subcomponent(modules = ActivityModule.class) 11 | public interface ActivityComponent { 12 | 13 | void inject(MainActivity mainActivity); 14 | 15 | void inject(DetailActivity detailActivity); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/component/AppComponent.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection.component; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Component; 9 | import io.mvpstarter.sample.data.DataManager; 10 | import io.mvpstarter.sample.injection.ApplicationContext; 11 | import io.mvpstarter.sample.injection.module.AppModule; 12 | 13 | @Singleton 14 | @Component(modules = AppModule.class) 15 | public interface AppComponent { 16 | 17 | @ApplicationContext 18 | Context context(); 19 | 20 | Application application(); 21 | 22 | DataManager apiManager(); 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/component/ConfigPersistentComponent.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection.component; 2 | 3 | import dagger.Component; 4 | import io.mvpstarter.sample.features.base.BaseActivity; 5 | import io.mvpstarter.sample.features.base.BaseFragment; 6 | import io.mvpstarter.sample.injection.ConfigPersistent; 7 | import io.mvpstarter.sample.injection.module.ActivityModule; 8 | import io.mvpstarter.sample.injection.module.FragmentModule; 9 | 10 | /** 11 | * A dagger component that will live during the lifecycle of an Activity or Fragment but it won't be 12 | * destroy during configuration changes. Check {@link BaseActivity} and {@link BaseFragment} to see 13 | * how this components survives configuration changes. Use the {@link ConfigPersistent} scope to 14 | * annotate dependencies that need to survive configuration changes (for example Presenters). 15 | */ 16 | @ConfigPersistent 17 | @Component(dependencies = AppComponent.class) 18 | public interface ConfigPersistentComponent { 19 | 20 | ActivityComponent activityComponent(ActivityModule activityModule); 21 | 22 | FragmentComponent fragmentComponent(FragmentModule fragmentModule); 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/component/FragmentComponent.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection.component; 2 | 3 | import dagger.Subcomponent; 4 | import io.mvpstarter.sample.injection.PerFragment; 5 | import io.mvpstarter.sample.injection.module.FragmentModule; 6 | 7 | /** 8 | * This component inject dependencies to all Fragments across the application 9 | */ 10 | @PerFragment 11 | @Subcomponent(modules = FragmentModule.class) 12 | public interface FragmentComponent { 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/module/ActivityModule.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection.module; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | 6 | import dagger.Module; 7 | import dagger.Provides; 8 | import io.mvpstarter.sample.injection.ActivityContext; 9 | 10 | @Module 11 | public class ActivityModule { 12 | 13 | private Activity activity; 14 | 15 | public ActivityModule(Activity activity) { 16 | this.activity = activity; 17 | } 18 | 19 | @Provides 20 | Activity provideActivity() { 21 | return activity; 22 | } 23 | 24 | @Provides 25 | @ActivityContext 26 | Context providesContext() { 27 | return activity; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/module/ApiModule.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection.module; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import dagger.Module; 6 | import dagger.Provides; 7 | import io.mvpstarter.sample.data.remote.PokemonService; 8 | import retrofit2.Retrofit; 9 | 10 | /** 11 | * Created by shivam on 29/5/17. 12 | */ 13 | @Module(includes = {NetworkModule.class}) 14 | public class ApiModule { 15 | 16 | @Provides 17 | @Singleton 18 | PokemonService providePokemonApi(Retrofit retrofit) { 19 | return retrofit.create(PokemonService.class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/module/AppModule.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection.module; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | 7 | import dagger.Module; 8 | import dagger.Provides; 9 | import io.mvpstarter.sample.injection.ApplicationContext; 10 | 11 | import static io.mvpstarter.sample.Constants.PREF_FILE_NAME; 12 | 13 | @Module(includes = {ApiModule.class}) 14 | public class AppModule { 15 | private final Application application; 16 | 17 | public AppModule(Application application) { 18 | this.application = application; 19 | } 20 | 21 | @Provides 22 | Application provideApplication() { 23 | return application; 24 | } 25 | 26 | @Provides 27 | @ApplicationContext 28 | Context provideContext() { 29 | return application; 30 | } 31 | 32 | @Provides 33 | @ApplicationContext 34 | SharedPreferences provideSharedPreference(@ApplicationContext Context context) { 35 | return context.getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/module/FragmentModule.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection.module; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.support.v4.app.Fragment; 6 | 7 | import dagger.Module; 8 | import dagger.Provides; 9 | import io.mvpstarter.sample.injection.ActivityContext; 10 | 11 | @Module 12 | public class FragmentModule { 13 | private Fragment fragment; 14 | 15 | public FragmentModule(Fragment fragment) { 16 | this.fragment = fragment; 17 | } 18 | 19 | @Provides 20 | Fragment providesFragment() { 21 | return fragment; 22 | } 23 | 24 | @Provides 25 | Activity provideActivity() { 26 | return fragment.getActivity(); 27 | } 28 | 29 | @Provides 30 | @ActivityContext 31 | Context providesContext() { 32 | return fragment.getActivity(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/injection/module/NetworkModule.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.injection.module; 2 | 3 | import android.content.Context; 4 | 5 | import com.facebook.stetho.okhttp3.StethoInterceptor; 6 | import com.google.gson.FieldNamingPolicy; 7 | import com.google.gson.Gson; 8 | import com.google.gson.GsonBuilder; 9 | import com.readystatesoftware.chuck.ChuckInterceptor; 10 | 11 | import javax.inject.Singleton; 12 | 13 | import dagger.Module; 14 | import dagger.Provides; 15 | import io.mvpstarter.sample.BuildConfig; 16 | import okhttp3.OkHttpClient; 17 | import okhttp3.logging.HttpLoggingInterceptor; 18 | import retrofit2.Retrofit; 19 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 20 | import retrofit2.converter.gson.GsonConverterFactory; 21 | import timber.log.Timber; 22 | 23 | /** 24 | * Created by shivam on 29/5/17. 25 | */ 26 | @Module 27 | public class NetworkModule { 28 | 29 | private final Context context; 30 | private final String baseUrl; 31 | 32 | public NetworkModule(final Context context, String baseUrl) { 33 | this.context = context; 34 | this.baseUrl = baseUrl; 35 | } 36 | 37 | @Provides 38 | @Singleton 39 | Retrofit provideRetrofit(OkHttpClient okHttpClient, Gson gson) { 40 | return new Retrofit.Builder() 41 | .baseUrl(baseUrl) 42 | .client(okHttpClient) 43 | .addConverterFactory(GsonConverterFactory.create(gson)) 44 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 45 | .build(); 46 | } 47 | 48 | @Provides 49 | @Singleton 50 | OkHttpClient provideOkHttpClient(HttpLoggingInterceptor httpLoggingInterceptor, 51 | StethoInterceptor stethoInterceptor, 52 | ChuckInterceptor chuckInterceptor) { 53 | OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); 54 | if (BuildConfig.DEBUG) { 55 | httpClientBuilder.addInterceptor(chuckInterceptor); 56 | httpClientBuilder.addInterceptor(httpLoggingInterceptor); 57 | httpClientBuilder.addNetworkInterceptor(stethoInterceptor); 58 | } 59 | return httpClientBuilder.build(); 60 | } 61 | 62 | @Provides 63 | @Singleton 64 | HttpLoggingInterceptor provideHttpLoggingInterceptor() { 65 | HttpLoggingInterceptor loggingInterceptor = 66 | new HttpLoggingInterceptor(message -> Timber.d(message)); 67 | loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 68 | return loggingInterceptor; 69 | } 70 | 71 | @Provides 72 | @Singleton 73 | StethoInterceptor provideStethoInterceptor() { 74 | return new StethoInterceptor(); 75 | } 76 | 77 | @Provides 78 | @Singleton 79 | ChuckInterceptor provideChuckInterceptor() { 80 | return new ChuckInterceptor(context); 81 | } 82 | 83 | @Provides 84 | @Singleton 85 | Gson provideGson() { 86 | return new GsonBuilder() 87 | .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") 88 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 89 | .create(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/util/NetworkUtil.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.NetworkInfo; 6 | 7 | import retrofit2.HttpException; 8 | 9 | public class NetworkUtil { 10 | 11 | /** 12 | * Returns true if the Throwable is an instance of RetrofitError with an http status code equals 13 | * to the given one. 14 | */ 15 | public static boolean isHttpStatusCode(Throwable throwable, int statusCode) { 16 | return throwable instanceof HttpException 17 | && ((HttpException) throwable).code() == statusCode; 18 | } 19 | 20 | public static boolean isNetworkConnected(Context context) { 21 | ConnectivityManager cm = 22 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 23 | NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); 24 | return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/util/ViewUtil.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.res.Resources; 6 | import android.view.inputmethod.InputMethodManager; 7 | 8 | public final class ViewUtil { 9 | 10 | public static float pxToDp(float px) { 11 | float densityDpi = Resources.getSystem().getDisplayMetrics().densityDpi; 12 | return px / (densityDpi / 160f); 13 | } 14 | 15 | public static int dpToPx(int dp) { 16 | float density = Resources.getSystem().getDisplayMetrics().density; 17 | return Math.round(dp * density); 18 | } 19 | 20 | public static void hideKeyboard(Activity activity) { 21 | InputMethodManager imm = 22 | (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); 23 | imm.hideSoftInputFromWindow(activity.getWindow().getDecorView().getWindowToken(), 0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/util/rx/scheduler/BaseScheduler.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util.rx.scheduler; 2 | 3 | import org.reactivestreams.Publisher; 4 | 5 | import io.reactivex.Completable; 6 | import io.reactivex.CompletableSource; 7 | import io.reactivex.CompletableTransformer; 8 | import io.reactivex.Flowable; 9 | import io.reactivex.FlowableTransformer; 10 | import io.reactivex.Maybe; 11 | import io.reactivex.MaybeSource; 12 | import io.reactivex.MaybeTransformer; 13 | import io.reactivex.Observable; 14 | import io.reactivex.ObservableSource; 15 | import io.reactivex.ObservableTransformer; 16 | import io.reactivex.Scheduler; 17 | import io.reactivex.Single; 18 | import io.reactivex.SingleSource; 19 | import io.reactivex.SingleTransformer; 20 | 21 | /** 22 | * Created by lam on 2/6/17. 23 | */ 24 | public abstract class BaseScheduler 25 | implements ObservableTransformer, 26 | SingleTransformer, 27 | MaybeTransformer, 28 | CompletableTransformer, 29 | FlowableTransformer { 30 | 31 | private final Scheduler subscribeOnScheduler; 32 | 33 | private final Scheduler observeOnScheduler; 34 | 35 | protected BaseScheduler(Scheduler subscribeOnScheduler, Scheduler observeOnScheduler) { 36 | this.subscribeOnScheduler = subscribeOnScheduler; 37 | this.observeOnScheduler = observeOnScheduler; 38 | } 39 | 40 | @Override 41 | public CompletableSource apply(Completable upstream) { 42 | return upstream.subscribeOn(subscribeOnScheduler).observeOn(observeOnScheduler); 43 | } 44 | 45 | @Override 46 | public Publisher apply(Flowable upstream) { 47 | return upstream.subscribeOn(subscribeOnScheduler).observeOn(observeOnScheduler); 48 | } 49 | 50 | @Override 51 | public MaybeSource apply(Maybe upstream) { 52 | return upstream.subscribeOn(subscribeOnScheduler).observeOn(observeOnScheduler); 53 | } 54 | 55 | @Override 56 | public ObservableSource apply(Observable upstream) { 57 | return upstream.subscribeOn(subscribeOnScheduler).observeOn(observeOnScheduler); 58 | } 59 | 60 | @Override 61 | public SingleSource apply(Single upstream) { 62 | return upstream.subscribeOn(subscribeOnScheduler).observeOn(observeOnScheduler); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/util/rx/scheduler/ComputationMainScheduler.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util.rx.scheduler; 2 | 3 | import io.reactivex.android.schedulers.AndroidSchedulers; 4 | import io.reactivex.schedulers.Schedulers; 5 | 6 | /** 7 | * Created by lam on 2/6/17. 8 | */ 9 | public class ComputationMainScheduler extends BaseScheduler { 10 | 11 | protected ComputationMainScheduler() { 12 | super(Schedulers.computation(), AndroidSchedulers.mainThread()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/util/rx/scheduler/IoMainScheduler.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util.rx.scheduler; 2 | 3 | import io.reactivex.android.schedulers.AndroidSchedulers; 4 | import io.reactivex.schedulers.Schedulers; 5 | 6 | /** 7 | * Created by lam on 2/6/17. 8 | */ 9 | public class IoMainScheduler extends BaseScheduler { 10 | 11 | public IoMainScheduler() { 12 | super(Schedulers.io(), AndroidSchedulers.mainThread()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/util/rx/scheduler/NewThreadMainScheduler.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util.rx.scheduler; 2 | 3 | import io.reactivex.android.schedulers.AndroidSchedulers; 4 | import io.reactivex.schedulers.Schedulers; 5 | 6 | /** 7 | * Created by lam on 2/6/17. 8 | */ 9 | public class NewThreadMainScheduler extends BaseScheduler { 10 | 11 | protected NewThreadMainScheduler() { 12 | super(Schedulers.newThread(), AndroidSchedulers.mainThread()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/util/rx/scheduler/SchedulerUtils.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util.rx.scheduler; 2 | 3 | /** 4 | * Created by lam on 2/6/17. 5 | */ 6 | public class SchedulerUtils { 7 | 8 | public static IoMainScheduler ioToMain() { 9 | return new IoMainScheduler<>(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/util/rx/scheduler/SingleMainScheduler.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util.rx.scheduler; 2 | 3 | import io.reactivex.android.schedulers.AndroidSchedulers; 4 | import io.reactivex.schedulers.Schedulers; 5 | 6 | /** 7 | * Created by lam on 2/6/17. 8 | */ 9 | public class SingleMainScheduler extends BaseScheduler { 10 | 11 | protected SingleMainScheduler() { 12 | super(Schedulers.single(), AndroidSchedulers.mainThread()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/mvpstarter/sample/util/rx/scheduler/TrampolineMainScheduler.java: -------------------------------------------------------------------------------- 1 | package io.mvpstarter.sample.util.rx.scheduler; 2 | 3 | import io.reactivex.android.schedulers.AndroidSchedulers; 4 | import io.reactivex.schedulers.Schedulers; 5 | 6 | /** 7 | * Created by lam on 2/6/17. 8 | */ 9 | public class TrampolineMainScheduler extends BaseScheduler { 10 | 11 | protected TrampolineMainScheduler() { 12 | super(Schedulers.trampoline(), AndroidSchedulers.mainThread()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 18 | 19 | 20 | 21 | 26 | 27 | 34 | 35 | 44 | 45 | 46 | 47 | 52 | 53 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 17 | 18 | 19 | 20 | 25 | 26 | 30 | 31 | 32 | 33 | 38 | 39 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_maps_sample.xml: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_pokemon.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 22 | 23 |