├── .gitignore ├── .travis.yml ├── AndroidStarter.iml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ ├── java │ │ └── fr │ │ │ └── guddy │ │ │ └── androidstarter │ │ │ └── tests │ │ │ ├── mock │ │ │ ├── MockApplication.java │ │ │ ├── MockDatabaseHelperAndroidStarter.java │ │ │ ├── MockModuleDatabase.java │ │ │ ├── MockModuleEnvironment.java │ │ │ └── MockModuleRest.java │ │ │ ├── parsing │ │ │ └── TestParsing.java │ │ │ ├── rest │ │ │ └── TestREST.java │ │ │ ├── runner │ │ │ └── AndroidStarterTestRunner.java │ │ │ └── ui │ │ │ ├── AbstractRobotiumTestCase.java │ │ │ └── TestActivityRepoList.java │ └── res │ │ └── raw │ │ ├── repo_octocat.json │ │ └── repos_octocat.json │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── fr │ │ └── guddy │ │ └── androidstarter │ │ ├── ApplicationAndroidStarter.java │ │ ├── Environment.java │ │ ├── IEnvironment.java │ │ ├── bus │ │ ├── BusMainThread.java │ │ ├── BusManager.java │ │ └── event │ │ │ ├── AbstractEvent.java │ │ │ └── AbstractEventQueryDidFinish.java │ │ ├── di │ │ ├── modules │ │ │ ├── ModuleAsync.java │ │ │ ├── ModuleBus.java │ │ │ ├── ModuleContext.java │ │ │ ├── ModuleDatabase.java │ │ │ ├── ModuleEnvironment.java │ │ │ ├── ModuleRest.java │ │ │ └── ModuleTransformer.java │ │ └── scopes │ │ │ └── FragmentScope.java │ │ ├── mvp │ │ ├── repo_detail │ │ │ ├── ActivityRepoDetail.java │ │ │ ├── FragmentRepoDetail.java │ │ │ ├── InteractorRepoDetail.java │ │ │ ├── MvpRepoDetail.java │ │ │ └── PresenterRepoDetail.java │ │ └── repo_list │ │ │ ├── ActivityRepoList.java │ │ │ ├── CellRepo.java │ │ │ ├── FragmentRepoList.java │ │ │ ├── PresenterRepoList.java │ │ │ └── RepoListMvp.java │ │ ├── persistence │ │ ├── DatabaseHelperAndroidStarter.java │ │ ├── dao │ │ │ ├── AbstractBaseDAOImpl.java │ │ │ ├── DAORepo.java │ │ │ ├── IOrmLiteEntityDAO.java │ │ │ ├── IRxDao.java │ │ │ └── RxBaseDaoImpl.java │ │ └── entities │ │ │ ├── AbstractOrmLiteEntity.java │ │ │ └── RepoEntity.java │ │ └── rest │ │ ├── GitHubService.java │ │ ├── dto │ │ ├── DTOOwner.java │ │ └── DTORepo.java │ │ ├── error_handling │ │ └── RetrofitException.java │ │ └── queries │ │ ├── AbstractQuery.java │ │ └── QueryGetRepos.java │ └── res │ ├── drawable │ └── git_icon.png │ ├── layout │ ├── activity_repo_detail.xml │ ├── activity_repo_list.xml │ ├── cell_repo.xml │ ├── fragment_repo_detail.xml │ └── fragment_repo_list.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 │ ├── raw │ └── ormlite_config.txt │ ├── values-v21 │ └── styles.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── assets ├── code_coverage_report.png ├── logo_android.png ├── logo_arrow.jpg ├── logo_fluentview.jpeg ├── logo_frutilla.jpg ├── logo_mockito.png ├── logo_ormlite.png ├── logo_powermock.png ├── logo_robolectric.png ├── logo_robotium.png ├── logo_transformer.png └── originals │ ├── code_coverage_report.tiff │ ├── logo_android.png │ ├── logo_arrow.jpg │ ├── logo_fluentview.jpeg │ ├── logo_frutilla.jpg │ ├── logo_mockito.png │ ├── logo_ormlite.png │ ├── logo_powermock.png │ ├── logo_robolectric.png │ ├── logo_robotium.png │ └── logo_transformer.png ├── build.gradle ├── circle.yml ├── config ├── quality.gradle └── quality │ ├── checkstyle │ ├── checkstyle.xml │ └── suppressions.xml │ ├── findbugs │ └── findbugs-filter.xml │ ├── lint │ └── lint.xml │ └── pmd │ └── pmd-ruleset.xml ├── docs ├── Makefile ├── README.md ├── _config.yml ├── architecture.md ├── architecture.pdf ├── assets │ ├── code_coverage_report.png │ ├── logo_android.png │ ├── logo_arrow.jpg │ ├── logo_fluentview.jpeg │ ├── logo_frutilla.jpg │ ├── logo_mockito.png │ ├── logo_ormlite.png │ ├── logo_powermock.png │ ├── logo_robolectric.png │ ├── logo_robotium.png │ ├── logo_transformer.png │ └── originals │ │ ├── code_coverage_report.tiff │ │ ├── logo_android.png │ │ ├── logo_arrow.jpg │ │ ├── logo_fluentview.jpeg │ │ ├── logo_frutilla.jpg │ │ ├── logo_mockito.png │ │ ├── logo_ormlite.png │ │ ├── logo_powermock.png │ │ ├── logo_robolectric.png │ │ ├── logo_robotium.png │ │ └── logo_transformer.png ├── listings_setup.tex └── template.tex ├── environmentSetup.sh ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jacoco.gradle └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | assets/.DS_Store 3 | 4 | .DS_Store 5 | 6 | local.properties 7 | 8 | .idea 9 | 10 | .gradle 11 | 12 | build 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | android: 7 | components: 8 | - tools # to get the new `repository-11.xml` 9 | - tools # see https://github.com/travis-ci/travis-ci/issues/6040#issuecomment-219367943) 10 | - platform-tools 11 | - build-tools-23.0.3 12 | - android-25 13 | - extra-android-support 14 | - extra-android-m2repository 15 | # Specify at least one system image, 16 | # if you need to run emulator(s) during your tests 17 | - sys-img-armeabi-v7a-android-21 18 | licenses: 19 | - 'android-sdk-license-.+' 20 | 21 | env: 22 | global: 23 | # install timeout in minutes (2 minutes by default) 24 | - ADB_INSTALL_TIMEOUT=8 25 | 26 | before_install: 27 | - echo yes | android update sdk --filter extra-android-support --no-ui --force > /dev/null 28 | - echo yes | android update sdk --filter extra-android-m2repository --no-ui --force > /dev/null 29 | 30 | before_script: 31 | - echo no | android create avd --force -n test -t android-21 --abi armeabi-v7a 32 | - emulator -avd test -no-skin -no-audio -no-window & 33 | - android-wait-for-emulator 34 | - adb shell input keyevent 82 & 35 | 36 | script: 37 | - android list target 38 | - ./gradlew build connectedCheck 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [2016] [Romain Rochegude] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidStarter 2 | A sample Android app using the MVP architecture. 3 | 4 | [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AndroidStarter-brightgreen.svg?style=flat)](https://android-arsenal.com/details/3/4936) 5 | [![Android Weekly](https://img.shields.io/badge/Android%20Weekly-%23204-green.svg)](http://androidweekly.net/issues/issue-204) 6 | 7 | [![Build Status](https://travis-ci.org/RoRoche/AndroidStarter.svg?branch=master)](https://travis-ci.org/RoRoche/AndroidStarter) 8 | [![Build Status](https://circleci.com/gh/RoRoche/AndroidStarter.svg?style=shield&circle-token=e1392aa8f9f0e28e84fcbe56e7799aa0dad35142)](https://circleci.com/gh/RoRoche/AndroidStarter) 9 | [![Build Status](https://www.bitrise.io/app/4bb734986df5e64f.svg?token=Qhm_4tcy5Zg8fO6YbsKGHQ&branch=master)](https://www.bitrise.io/app/4bb734986df5e64f) 10 | [![Code coverage](https://codecov.io/github/RoRoche/AndroidStarter/coverage.svg?branch=master)](https://codecov.io/gh/RoRoche/AndroidStarter) 11 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a21bb4740cba4ce1b166d3a7c3578c0e)](https://www.codacy.com/app/romain-rochegude_2/AndroidStarter?utm_source=github.com&utm_medium=referral&utm_content=RoRoche/AndroidStarter&utm_campaign=Badge_Grade) 12 | [![Dependency Status](https://www.versioneye.com/user/projects/5818f46e89f0a91dbb44ae9d/badge.svg?style=flat-square)](https://www.versioneye.com/user/projects/5818f46e89f0a91dbb44ae9d) 13 | 14 | 15 | 16 | ## AndroidStarter in the news 17 | 18 | * [Android Weekly #204](http://androidweekly.net/issues/issue-204) 19 | 20 | ## TODO 21 | 22 | - [ ] move from MVP to VIPER for `ListRepo` module 23 | 24 | ## See also 25 | 26 | * : an alternative project using 27 | * [requery](https://github.com/requery/requery/) as persistence layer 28 | * [EventBus](https://github.com/greenrobot/EventBus) as event bus layer 29 | * [LoganSquare](https://github.com/bluelinelabs/LoganSquare) as JSON parsing layer 30 | * [Conductor](https://github.com/bluelinelabs/Conductor) to build View-based Android application 31 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | build/* 3 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | jcenter() 5 | maven { 6 | url 'https://oss.sonatype.org/content/repositories/snapshots' 7 | } 8 | } 9 | 10 | dependencies { 11 | classpath 'com.github.stephanenicolas.ormgap:ormgap-plugin:1.0.11' 12 | } 13 | } 14 | 15 | apply plugin: 'com.android.application' 16 | 17 | apply plugin: 'com.fernandocejas.frodo' 18 | apply plugin: 'com.jakewharton.hugo' 19 | apply plugin: 'com.neenbedankt.android-apt' 20 | apply plugin: 'com.github.ben-manes.versions' 21 | 22 | apply plugin: 'me.tatarka.retrolambda' 23 | 24 | apply plugin: 'ormgap' 25 | 26 | apply from: '../jacoco.gradle' 27 | 28 | //apply from: '../config/quality.gradle' 29 | 30 | repositories { 31 | maven { 32 | url "https://jitpack.io" 33 | } 34 | 35 | mavenCentral() 36 | 37 | maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } 38 | } 39 | 40 | android { 41 | compileSdkVersion 25 42 | buildToolsVersion "23.0.3" 43 | 44 | def ENVIRONMENT_CONSTANT_TYPE = "fr.guddy.androidstarter.IEnvironment" 45 | def ENVIRONMENT_CONSTANT_VALUE_DEV = "fr.guddy.androidstarter.Environment.DEV" 46 | def ENVIRONMENT_CONSTANT_VALUE_PROD = "fr.guddy.androidstarter.Environment.PROD" 47 | def ENVIRONMENT_CONSTANT_NAME = "ENVIRONMENT" 48 | 49 | defaultConfig { 50 | applicationId "fr.guddy.androidstarter" 51 | minSdkVersion 18 52 | targetSdkVersion 25 53 | versionCode 1 54 | versionName "1.0" 55 | // Enabling multidex support. 56 | multiDexEnabled true 57 | 58 | testInstrumentationRunner "fr.guddy.androidstarter.tests.runner.AndroidStarterTestRunner" 59 | } 60 | 61 | buildTypes { 62 | debug { 63 | testCoverageEnabled = true 64 | buildConfigField ENVIRONMENT_CONSTANT_TYPE, ENVIRONMENT_CONSTANT_NAME, ENVIRONMENT_CONSTANT_VALUE_DEV 65 | } 66 | release { 67 | minifyEnabled false 68 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 69 | buildConfigField ENVIRONMENT_CONSTANT_TYPE, ENVIRONMENT_CONSTANT_NAME, ENVIRONMENT_CONSTANT_VALUE_PROD 70 | } 71 | } 72 | 73 | packagingOptions { 74 | exclude 'META-INF/services/javax.annotation.processing.Processor' 75 | exclude 'META-INF/LICENSE' 76 | exclude 'META-INF/NOTICE' 77 | exclude 'META-INF/services/javax.annotation.processing.Processor' 78 | } 79 | 80 | lintOptions { 81 | disable 'InvalidPackage' 82 | } 83 | 84 | compileOptions { 85 | sourceCompatibility JavaVersion.VERSION_1_8 86 | targetCompatibility JavaVersion.VERSION_1_8 87 | } 88 | } 89 | 90 | dependencies { 91 | compile fileTree(dir: 'libs', include: ['*.jar']) 92 | compile 'com.android.support:appcompat-v7:25.1.1' 93 | compile 'com.android.support:support-v4:25.1.1' 94 | compile 'com.android.support:design:25.1.1' 95 | 96 | compile 'com.android.support:multidex:1.0.1' 97 | 98 | compile 'com.github.lukaspili.autodagger2:autodagger2:1.2-SNAPSHOT' 99 | apt 'com.github.lukaspili.autodagger2:autodagger2-compiler:1.2-SNAPSHOT' 100 | 101 | compile 'com.android.support:recyclerview-v7:25.1.1' 102 | 103 | compile 'com.jakewharton:butterknife:8.5.1' 104 | apt 'com.jakewharton:butterknife-compiler:8.5.1' 105 | 106 | compile 'com.hannesdorfmann.fragmentargs:annotation:3.0.2' 107 | apt 'com.hannesdorfmann.fragmentargs:processor:3.0.2' 108 | 109 | compile 'se.emilsjolander:intentbuilder-api:0.14.0' 110 | apt 'se.emilsjolander:intentbuilder-compiler:0.14.0' 111 | 112 | compile 'com.github.frankiesardo:icepick:2.3.6' 113 | apt 'com.github.frankiesardo:icepick-processor:2.3.6' 114 | 115 | compile 'io.nlopez.smartadapters:library:1.3.1' 116 | 117 | compile('com.github.jkwiecien:Switcher:1.1.3') { 118 | exclude module: 'appcompat-v7' 119 | } 120 | 121 | compile 'com.squareup.okhttp3:okhttp:3.6.0' 122 | compile 'com.squareup.okhttp3:okhttp-urlconnection:3.6.0' 123 | compile 'com.squareup.okhttp3:logging-interceptor:3.6.0' 124 | compile 'com.squareup.retrofit2:retrofit:2.2.0' 125 | compile 'com.squareup.retrofit2:converter-jackson:2.2.0' 126 | 127 | compile 'com.google.dagger:dagger:2.9' 128 | apt 'com.google.dagger:dagger-compiler:2.9' 129 | 130 | provided 'javax.annotation:jsr250-api:1.0' 131 | 132 | compile 'com.j256.ormlite:ormlite-core:5.0' 133 | compile 'com.j256.ormlite:ormlite-android:5.0' 134 | 135 | compile 'com.github.orhanobut:logger:1.12' 136 | 137 | compile 'io.reactivex:rxandroid:1.2.1' 138 | compile 'io.reactivex:rxjava:1.2.7' 139 | 140 | compile 'com.mobandme:android-transformer:1.2' 141 | provided 'com.mobandme:android-transformer-compiler:1.2' 142 | 143 | compile 'com.birbit:android-priority-jobqueue:2.0.1' 144 | 145 | compile 'com.squareup:otto:1.3.8' 146 | 147 | compile('com.novoda:merlin:0.8.0') { 148 | exclude group: 'io.reactivex', module: 'rxandroid' 149 | } 150 | 151 | compile 'com.hannesdorfmann.mosby:mvp:2.0.1' 152 | compile 'com.hannesdorfmann.mosby:viewstate:2.0.1' 153 | 154 | compile('com.github.polok.localify:localify:1.0.0') { 155 | exclude group: 'io.reactivex', module: 'rxjava' 156 | } 157 | 158 | compile 'com.squareup.picasso:picasso:2.5.2' 159 | 160 | // DebugDrawer specific dependencies 161 | debugCompile 'io.palaima.debugdrawer:debugdrawer:0.7.0' 162 | releaseCompile 'io.palaima.debugdrawer:debugdrawer-no-op:0.7.0' 163 | debugCompile 'io.palaima.debugdrawer:debugdrawer-view:0.7.0' 164 | releaseCompile 'io.palaima.debugdrawer:debugdrawer-view-no-op:0.7.0' 165 | compile 'io.palaima.debugdrawer:debugdrawer-commons:0.7.0' 166 | compile 'io.palaima.debugdrawer:debugdrawer-scalpel:0.7.0' 167 | compile 'io.palaima.debugdrawer:debugdrawer-picasso:0.7.0' 168 | compile 'io.palaima.debugdrawer:debugdrawer-fps:0.7.0' 169 | compile 'com.jakewharton.scalpel:scalpel:1.1.2' 170 | compile 'jp.wasabeef:takt:1.0.2' 171 | 172 | // Testing-only dependencies 173 | testCompile 'junit:junit:4.12' 174 | androidTestCompile('com.android.support:multidex-instrumentation:1.0.1') { 175 | exclude group: 'com.android.support', module: 'multidex' 176 | } 177 | androidTestCompile 'com.android.support:support-annotations:25.1.1' 178 | androidTestCompile 'com.android.support.test:runner:0.5' 179 | androidTestCompile 'com.android.support.test:rules:0.5' 180 | androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.6.3' 181 | androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.6.0' 182 | androidTestCompile 'com.google.truth:truth:0.32' 183 | androidTestCompile 'com.github.ignaciotcrespo:frutilla:0.7.1' 184 | androidTestCompile 'org.mockito:mockito-core:2.0.17-beta' 185 | androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' 186 | androidTestCompile 'com.squareup.okhttp3:okhttp-testing-support:3.6.0' 187 | 188 | // Optional -- Hamcrest library 189 | androidTestCompile 'org.hamcrest:hamcrest-library:1.3' 190 | // Optional -- UI testing with Espresso 191 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' 192 | // Optional -- UI testing with UI Automator 193 | androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2' 194 | } 195 | -------------------------------------------------------------------------------- /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 {ADT_HOME}/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | -keep class butterknife.** { *; } 20 | -dontwarn butterknife.internal.** 21 | -keep class **$$ViewBinder { *; } 22 | 23 | -keepclasseswithmembernames class * { 24 | @butterknife.* ; 25 | } 26 | 27 | -keepclasseswithmembernames class * { 28 | @butterknife.* ; 29 | } 30 | 31 | -dontwarn icepick.** 32 | -keep class **$$Icepick { *; } 33 | -keepclasseswithmembernames class * { 34 | @icepick.* ; 35 | } 36 | 37 | -keepattributes *Annotation* 38 | -keepclassmembers class ** { 39 | @com.squareup.otto.Subscribe public *; 40 | @com.squareup.otto.Produce public *; 41 | } 42 | 43 | -dontwarn retrofit2.** 44 | -keep class retrofit2.** { *; } 45 | -keepattributes Signature 46 | -keepattributes Exceptions -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/guddy/androidstarter/tests/mock/MockApplication.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.tests.mock; 2 | 3 | import fr.guddy.androidstarter.ApplicationAndroidStarter; 4 | import fr.guddy.androidstarter.DaggerApplicationAndroidStarterComponent; 5 | import fr.guddy.androidstarter.di.modules.ModuleAsync; 6 | import fr.guddy.androidstarter.di.modules.ModuleBus; 7 | import fr.guddy.androidstarter.di.modules.ModuleContext; 8 | import fr.guddy.androidstarter.di.modules.ModuleEnvironment; 9 | import fr.guddy.androidstarter.di.modules.ModuleTransformer; 10 | 11 | public class MockApplication extends ApplicationAndroidStarter { 12 | 13 | //region Fields 14 | private ModuleBus mModuleBus; 15 | private MockModuleRest mModuleRest; 16 | private ModuleEnvironment mModuleEnvironment; 17 | //endregion 18 | 19 | //region Singleton 20 | protected static MockApplication sSharedMockApplication; 21 | private ModuleAsync mModuleAsync; 22 | 23 | public static MockApplication sharedMockApplication() { 24 | return sSharedMockApplication; 25 | } 26 | //endregion 27 | 28 | //region Lifecycle 29 | @Override 30 | public void onCreate() { 31 | super.onCreate(); 32 | sSharedMockApplication = this; 33 | } 34 | //endregion 35 | 36 | //region Overridden method 37 | @Override 38 | protected void buildComponent() { 39 | mModuleAsync = new ModuleAsync(); 40 | mModuleBus = new ModuleBus(); 41 | mModuleRest = new MockModuleRest(); 42 | mModuleEnvironment = new MockModuleEnvironment(); 43 | 44 | mComponentApplication = DaggerApplicationAndroidStarterComponent.builder() 45 | .moduleAsync(mModuleAsync) 46 | .moduleBus(mModuleBus) 47 | .moduleContext(new ModuleContext(getApplicationContext())) 48 | .moduleDatabase(new MockModuleDatabase()) 49 | .moduleEnvironment(mModuleEnvironment) 50 | .moduleRest(mModuleRest) 51 | .moduleTransformer(new ModuleTransformer()) 52 | .build(); 53 | } 54 | //endregion 55 | 56 | //region Getters 57 | public ModuleAsync getModuleAsync() { 58 | return mModuleAsync; 59 | } 60 | 61 | public MockModuleRest getModuleRest() { 62 | return mModuleRest; 63 | } 64 | 65 | public ModuleBus getModuleBus() { 66 | return mModuleBus; 67 | } 68 | 69 | public ModuleEnvironment getModuleEnvironment() { 70 | return mModuleEnvironment; 71 | } 72 | //endregion 73 | } 74 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/guddy/androidstarter/tests/mock/MockDatabaseHelperAndroidStarter.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.tests.mock; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import fr.guddy.androidstarter.persistence.DatabaseHelperAndroidStarter; 9 | 10 | @Singleton 11 | public class MockDatabaseHelperAndroidStarter extends DatabaseHelperAndroidStarter { 12 | private static final String DATABASE_NAME = "mock_android_starter.db"; 13 | private static final int DATABASE_VERSION = 1; 14 | 15 | //region Constructor 16 | public MockDatabaseHelperAndroidStarter(@NonNull final Context poContext) { 17 | super(poContext, DATABASE_NAME, null, DATABASE_VERSION); 18 | } 19 | //endregion 20 | } 21 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/guddy/androidstarter/tests/mock/MockModuleDatabase.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.tests.mock; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | import fr.guddy.androidstarter.di.modules.ModuleDatabase; 11 | import fr.guddy.androidstarter.persistence.DatabaseHelperAndroidStarter; 12 | 13 | @Module 14 | public class MockModuleDatabase extends ModuleDatabase { 15 | 16 | @Provides 17 | @Singleton 18 | public DatabaseHelperAndroidStarter provideDatabaseHelperAndroidStarter(@NonNull final Context poContext) { 19 | return new MockDatabaseHelperAndroidStarter(poContext); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/guddy/androidstarter/tests/mock/MockModuleEnvironment.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.tests.mock; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import dagger.Module; 6 | import dagger.Provides; 7 | import fr.guddy.androidstarter.Environment; 8 | import fr.guddy.androidstarter.di.modules.ModuleEnvironment; 9 | 10 | @Module 11 | public class MockModuleEnvironment extends ModuleEnvironment { 12 | 13 | @Provides 14 | @Singleton 15 | public Environment provideEnvironment() { 16 | return Environment.TEST; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/guddy/androidstarter/tests/mock/MockModuleRest.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.tests.mock; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.squareup.picasso.Picasso; 7 | 8 | import dagger.Module; 9 | import fr.guddy.androidstarter.di.modules.ModuleRest; 10 | import fr.guddy.androidstarter.rest.GitHubService; 11 | import io.palaima.debugdrawer.picasso.PicassoModule; 12 | import okhttp3.OkHttpClient; 13 | import okhttp3.mockwebserver.MockWebServer; 14 | import retrofit2.Retrofit; 15 | import retrofit2.converter.jackson.JacksonConverterFactory; 16 | 17 | import static org.mockito.Mockito.mock; 18 | 19 | @Module 20 | public class MockModuleRest extends ModuleRest { 21 | //region Fields 22 | private MockWebServer mMockWebServer; 23 | private final Picasso mPicasso = mock(Picasso.class); 24 | private final PicassoModule mPicassoModule = mock(PicassoModule.class); 25 | //endregion 26 | 27 | //region Constructor 28 | public MockModuleRest() { 29 | mMockWebServer = new MockWebServer(); 30 | } 31 | //endregion 32 | 33 | //region Modules 34 | @Override 35 | public GitHubService provideGithubService(@NonNull final OkHttpClient poOkHttpClient) { 36 | final Retrofit loRetrofit = new Retrofit.Builder() 37 | .baseUrl(mMockWebServer.url("/").toString()) 38 | .client(poOkHttpClient) 39 | .addConverterFactory(JacksonConverterFactory.create()) 40 | .build(); 41 | return loRetrofit.create(GitHubService.class); 42 | } 43 | 44 | @Override 45 | public Picasso providePicasso(@NonNull final Context poContext) { 46 | return mPicasso; 47 | } 48 | 49 | @Override 50 | public PicassoModule providePicassoModule(@NonNull final Picasso poPicasso) { 51 | return mPicassoModule; 52 | } 53 | //endregion 54 | 55 | //region Getters 56 | public MockWebServer getMockWebServer() { 57 | return mMockWebServer; 58 | } 59 | 60 | public Picasso getPicasso() { 61 | return mPicasso; 62 | } 63 | //endregion 64 | 65 | //region Visible API 66 | public void setUp() { 67 | mMockWebServer = new MockWebServer(); 68 | } 69 | //endregion 70 | } 71 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/guddy/androidstarter/tests/parsing/TestParsing.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.tests.parsing; 2 | 3 | import android.support.test.InstrumentationRegistry; 4 | import android.test.suitebuilder.annotation.LargeTest; 5 | 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.github.polok.localify.LocalifyClient; 8 | 9 | import org.frutilla.FrutillaTestRunner; 10 | import org.frutilla.annotations.Frutilla; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | 15 | import java.io.IOException; 16 | import java.util.ArrayList; 17 | 18 | import fr.guddy.androidstarter.rest.dto.DTORepo; 19 | import fr.guddy.androidstarter.test.R; 20 | 21 | import static com.google.common.truth.Truth.assertThat; 22 | 23 | @RunWith(FrutillaTestRunner.class) 24 | public class TestParsing { 25 | 26 | //region Fields 27 | private LocalifyClient mLocalifyClient; 28 | //endregion 29 | 30 | //region Test lifecycle 31 | @Before 32 | public void setUp() throws Exception { 33 | mLocalifyClient = new LocalifyClient.Builder() 34 | .withResources(InstrumentationRegistry.getContext().getResources()) 35 | .build(); 36 | } 37 | //endregion 38 | 39 | //region Test methods 40 | @Frutilla( 41 | Given = "A single GitHub repo from a JSON file", 42 | When = "Parsing this content with Jackson", 43 | Then = "It should have parsed a repo named \"git-consortium\"" 44 | ) 45 | @Test 46 | public void test_Parse_JSONRepo_ParsesRepoDTO() throws IOException { 47 | ObjectMapper loJSONMapper; 48 | String lsRepoData; 49 | Given: 50 | { 51 | loJSONMapper = new ObjectMapper(); 52 | lsRepoData = mLocalifyClient.localify().loadRawFile(R.raw.repo_octocat); 53 | } 54 | 55 | DTORepo loRepoDTO; 56 | When: 57 | { 58 | loRepoDTO = loJSONMapper.readValue(lsRepoData, DTORepo.class); 59 | } 60 | 61 | Then: 62 | { 63 | assertThat(loRepoDTO).isNotNull(); 64 | assertThat(loRepoDTO.name).isEqualTo("git-consortium"); 65 | } 66 | } 67 | 68 | public static final class DTORepos extends ArrayList { 69 | } 70 | 71 | @Frutilla( 72 | Given = "Multiple GitHub repos from a JSON file", 73 | When = "Parsing this content with Jackson", 74 | Then = "It should have parsed a repo named \"git-consortium\"" 75 | ) 76 | @Test 77 | public void test_Parse_JSONArrayRepo_ParsesRepoAsArrayDTO() throws IOException { 78 | ObjectMapper loJSONMapper; 79 | String lsRepoDataAsArray; 80 | Given: 81 | { 82 | loJSONMapper = new ObjectMapper(); 83 | lsRepoDataAsArray = mLocalifyClient.localify().loadRawFile(R.raw.repos_octocat); 84 | } 85 | 86 | DTORepos lloRepoAsArrayDTO; 87 | When: 88 | { 89 | lloRepoAsArrayDTO = loJSONMapper.readValue(lsRepoDataAsArray, DTORepos.class); 90 | } 91 | 92 | Then: 93 | { 94 | assertThat(lloRepoAsArrayDTO).hasSize(1); 95 | assertThat(lloRepoAsArrayDTO.get(0).name).isEqualTo("git-consortium"); 96 | } 97 | } 98 | //endregion 99 | } 100 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/guddy/androidstarter/tests/rest/TestREST.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.tests.rest; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.test.InstrumentationRegistry; 6 | 7 | import com.github.polok.localify.LocalifyClient; 8 | import com.squareup.otto.Subscribe; 9 | 10 | import org.frutilla.FrutillaTestRunner; 11 | import org.frutilla.annotations.Frutilla; 12 | import org.junit.After; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | 17 | import java.io.IOException; 18 | import java.util.List; 19 | import java.util.concurrent.CountDownLatch; 20 | 21 | import fr.guddy.androidstarter.di.modules.ModuleAsync; 22 | import fr.guddy.androidstarter.di.modules.ModuleBus; 23 | import fr.guddy.androidstarter.di.modules.ModuleEnvironment; 24 | import fr.guddy.androidstarter.rest.GitHubService; 25 | import fr.guddy.androidstarter.rest.dto.DTORepo; 26 | import fr.guddy.androidstarter.rest.queries.QueryGetRepos; 27 | import fr.guddy.androidstarter.test.R; 28 | import fr.guddy.androidstarter.tests.mock.MockApplication; 29 | import fr.guddy.androidstarter.tests.mock.MockModuleRest; 30 | import okhttp3.mockwebserver.MockResponse; 31 | import okhttp3.mockwebserver.MockWebServer; 32 | import retrofit2.Response; 33 | 34 | import static com.google.common.truth.Truth.assertThat; 35 | 36 | @RunWith(FrutillaTestRunner.class) 37 | public class TestREST { 38 | 39 | //region Fields 40 | private LocalifyClient mLocalifyClient; 41 | private Context mContextTarget; 42 | private CountDownLatch mCountDownLatch; 43 | private QueryGetRepos.EventQueryGetReposDidFinish mEvent; 44 | private ModuleAsync mModuleAsync; 45 | private ModuleBus mModuleBus; 46 | private ModuleEnvironment mModuleEnvironment; 47 | private MockModuleRest mModuleRest; 48 | private MockWebServer mMockWebServer; 49 | //endregion 50 | 51 | //region Test lifecycle 52 | @Before 53 | public void setUp() throws Exception { 54 | mContextTarget = InstrumentationRegistry.getTargetContext(); 55 | 56 | mModuleAsync = MockApplication.sharedMockApplication().getModuleAsync(); 57 | mModuleBus = MockApplication.sharedMockApplication().getModuleBus(); 58 | mModuleRest = MockApplication.sharedMockApplication().getModuleRest(); 59 | mModuleRest.setUp(); 60 | mModuleEnvironment = MockApplication.sharedMockApplication().getModuleEnvironment(); 61 | 62 | mModuleBus.provideBusManager().registerSubscriberToBusAnyThread(this); 63 | 64 | mLocalifyClient = new LocalifyClient.Builder() 65 | .withResources(InstrumentationRegistry.getContext().getResources()) 66 | .build(); 67 | 68 | mMockWebServer = mModuleRest.getMockWebServer(); 69 | } 70 | 71 | @After 72 | public void tearDown() throws Exception { 73 | mModuleBus.provideBusManager().unregisterSubscriberFromBusAnyThread(this); 74 | try { 75 | mMockWebServer.shutdown(); 76 | } catch (@NonNull final Exception loException) { 77 | loException.printStackTrace(); 78 | } 79 | } 80 | //endregion 81 | 82 | //region Test methods 83 | @Frutilla( 84 | Given = "A single GitHub repo from a API", 85 | When = "Execute query", 86 | Then = "It should have got a repo named \"git-consortium\"" 87 | ) 88 | @Test 89 | public void test_CallSyncListRepos_WithOneRepo_ReturnsOneRepo() throws IOException { 90 | GitHubService loGitHubService; 91 | Given: 92 | { 93 | final String lsOneRepoJSONData = mLocalifyClient.localify().loadRawFile(R.raw.repos_octocat); 94 | final MockResponse loMockResponseWithOneRepo = new MockResponse().setResponseCode(200); 95 | loMockResponseWithOneRepo.setBody(lsOneRepoJSONData); 96 | mMockWebServer.enqueue(loMockResponseWithOneRepo); 97 | try { 98 | mMockWebServer.start(4000); 99 | } catch (@NonNull final Exception loException) { 100 | loException.printStackTrace(); 101 | } 102 | loGitHubService = mModuleRest.provideGithubService(mModuleRest.provideOkHttpClient(mModuleEnvironment.provideEnvironment(), mContextTarget)); 103 | } 104 | 105 | Response> loResponseWithOneRepo; 106 | When: 107 | { 108 | loResponseWithOneRepo = loGitHubService 109 | .listRepos("test") 110 | .execute(); 111 | } 112 | 113 | Then: 114 | { 115 | final List lloResults = loResponseWithOneRepo.body(); 116 | assertThat(lloResults).hasSize(1); 117 | assertThat(lloResults.get(0).name).isEqualTo("git-consortium"); 118 | assertThat(mMockWebServer.getRequestCount()).isEqualTo(1); 119 | } 120 | } 121 | 122 | @Frutilla( 123 | Given = "A single GitHub repo from a API", 124 | When = "Execute query asynchronously", 125 | Then = "It should have got a repo named \"git-consortium\"" 126 | ) 127 | @Test 128 | public void test_CallAsyncListRepos_WithOneRepo_ReturnsOneRepo() throws IOException, InterruptedException { 129 | Given: 130 | { 131 | final String lsOneRepoJSONData = mLocalifyClient.localify().loadRawFile(R.raw.repos_octocat); 132 | final MockResponse loMockResponseWithOneRepo = new MockResponse().setResponseCode(200); 133 | loMockResponseWithOneRepo.setBody(lsOneRepoJSONData); 134 | mMockWebServer.enqueue(loMockResponseWithOneRepo); 135 | try { 136 | mMockWebServer.start(4000); 137 | } catch (@NonNull final Exception loException) { 138 | loException.printStackTrace(); 139 | } 140 | } 141 | 142 | When: 143 | { 144 | mModuleAsync.provideJobManager(mContextTarget).addJobInBackground(new QueryGetRepos("test", false)); 145 | mCountDownLatch = new CountDownLatch(1); 146 | mCountDownLatch.await(); 147 | } 148 | 149 | Then: 150 | { 151 | assertThat(mEvent).isNotNull(); 152 | assertThat(mEvent.success).isTrue(); 153 | assertThat(mEvent.results).hasSize(1); 154 | assertThat(mEvent.results.get(0).name).isEqualTo("git-consortium"); 155 | } 156 | } 157 | //endregion 158 | 159 | //region Event subscription 160 | @Subscribe 161 | public void onEventQueryGetReposDidFinish(final QueryGetRepos.EventQueryGetReposDidFinish poEvent) { 162 | mEvent = poEvent; 163 | mCountDownLatch.countDown(); 164 | } 165 | //endregion 166 | } 167 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/guddy/androidstarter/tests/runner/AndroidStarterTestRunner.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.tests.runner; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import android.support.multidex.MultiDex; 7 | import android.support.test.runner.AndroidJUnitRunner; 8 | 9 | import fr.guddy.androidstarter.tests.mock.MockApplication; 10 | 11 | public class AndroidStarterTestRunner extends AndroidJUnitRunner { 12 | 13 | //region Overridden method 14 | @Override 15 | public void onCreate(final Bundle poArguments) { 16 | MultiDex.install(this.getTargetContext()); 17 | super.onCreate(poArguments); 18 | } 19 | 20 | @Override 21 | public Application newApplication( 22 | final ClassLoader poClassLoader, 23 | final String psClassName, 24 | final Context poContext) throws InstantiationException, IllegalAccessException, ClassNotFoundException { 25 | return super.newApplication(poClassLoader, MockApplication.class.getName(), poContext); 26 | } 27 | //endregion 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/guddy/androidstarter/tests/ui/AbstractRobotiumTestCase.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.tests.ui; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.support.test.InstrumentationRegistry; 6 | import android.support.test.rule.ActivityTestRule; 7 | 8 | import com.robotium.solo.Solo; 9 | 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.Rule; 13 | 14 | public abstract class AbstractRobotiumTestCase { 15 | //region Rule 16 | @Rule 17 | public final ActivityTestRule mActivityTestRule; 18 | //endregion 19 | 20 | //region Fields 21 | protected Solo mSolo; 22 | protected TypeActivity mActivity; 23 | protected Context mContextTest; 24 | protected Context mContextTarget; 25 | //endregion 26 | 27 | //region Constructor 28 | protected AbstractRobotiumTestCase(final ActivityTestRule poActivityTestRule) { 29 | mActivityTestRule = poActivityTestRule; 30 | } 31 | //endregion 32 | 33 | //region Test lifecycle 34 | @Before 35 | public void setUp() throws Exception { 36 | mActivity = mActivityTestRule.getActivity(); 37 | mSolo = new Solo(InstrumentationRegistry.getInstrumentation(), mActivity); 38 | mContextTest = InstrumentationRegistry.getContext(); 39 | mContextTarget = InstrumentationRegistry.getTargetContext(); 40 | } 41 | 42 | @After 43 | public void tearDown() throws Exception { 44 | mSolo.finishOpenedActivities(); 45 | } 46 | //endregion 47 | } 48 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/guddy/androidstarter/tests/ui/TestActivityRepoList.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.tests.ui; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.test.rule.ActivityTestRule; 5 | 6 | import com.github.polok.localify.LocalifyClient; 7 | 8 | import org.frutilla.FrutillaTestRunner; 9 | import org.frutilla.annotations.Frutilla; 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | 15 | import fr.guddy.androidstarter.mvp.repo_detail.ActivityRepoDetail; 16 | import fr.guddy.androidstarter.mvp.repo_list.ActivityRepoList; 17 | import fr.guddy.androidstarter.tests.mock.MockApplication; 18 | import fr.guddy.androidstarter.tests.mock.MockModuleRest; 19 | import okhttp3.mockwebserver.MockResponse; 20 | import okhttp3.mockwebserver.MockWebServer; 21 | 22 | import static com.google.common.truth.Truth.assertThat; 23 | 24 | //@org.junit.Ignore 25 | @RunWith(FrutillaTestRunner.class) 26 | public class TestActivityRepoList extends AbstractRobotiumTestCase { 27 | 28 | //region Fields 29 | private LocalifyClient mLocalifyClient; 30 | private MockModuleRest mModuleRest; 31 | private MockWebServer mMockWebServer; 32 | //endregion 33 | 34 | //region Constructor matching super 35 | public TestActivityRepoList() { 36 | super(new ActivityTestRule<>(ActivityRepoList.class, true, false)); 37 | } 38 | //endregion 39 | 40 | //region Test lifecycle 41 | @Before 42 | @Override 43 | public void setUp() throws Exception { 44 | super.setUp(); 45 | mModuleRest = MockApplication.sharedMockApplication().getModuleRest(); 46 | mModuleRest.setUp(); 47 | mMockWebServer = mModuleRest.getMockWebServer(); 48 | 49 | mLocalifyClient = new LocalifyClient.Builder() 50 | .withResources(mContextTest.getResources()) 51 | .build(); 52 | } 53 | 54 | @After 55 | @Override 56 | public void tearDown() throws Exception { 57 | super.tearDown(); 58 | try { 59 | mMockWebServer.shutdown(); 60 | } catch (@NonNull final Exception loException) { 61 | loException.printStackTrace(); 62 | } 63 | } 64 | //endregion 65 | 66 | //region Test methods 67 | @Frutilla( 68 | Given = "A single GitHub repo from the API", 69 | When = "", 70 | Then = "It should display a repo named \"git-consortium\"" 71 | ) 72 | @Test 73 | public void test_ListRepos_WithOneRepo_DisplayListWithOnlyThisRepo() { 74 | Given: 75 | { 76 | final String lsOneRepoJSONData = mLocalifyClient.localify().loadRawFile(fr.guddy.androidstarter.test.R.raw.repos_octocat); 77 | final MockResponse loMockResponseWithOneRepo = new MockResponse().setResponseCode(200); 78 | loMockResponseWithOneRepo.setBody(lsOneRepoJSONData); 79 | mMockWebServer.enqueue(loMockResponseWithOneRepo); 80 | try { 81 | mMockWebServer.start(4000); 82 | } catch (@NonNull final Exception loException) { 83 | loException.printStackTrace(); 84 | } 85 | mActivity = mActivityTestRule.launchActivity(null); 86 | } 87 | 88 | When: 89 | { 90 | } 91 | 92 | Then: 93 | { 94 | final boolean lbFoundTheRepo = mSolo.waitForText("git-consortium", 1, 5000L, true); 95 | assertThat(lbFoundTheRepo).isTrue(); 96 | } 97 | } 98 | 99 | @Frutilla( 100 | Given = "A single GitHub repo from the API", 101 | When = "Click on its name", 102 | Then = "It should display the detail of this repo" 103 | ) 104 | @Test 105 | public void test_ListRepos_ClickOnOneRepo_DisplayDetailWithOnlyThisRepo() { 106 | Given: 107 | { 108 | final String lsOneRepoJSONData = mLocalifyClient.localify().loadRawFile(fr.guddy.androidstarter.test.R.raw.repos_octocat); 109 | final MockResponse loMockResponseWithOneRepo = new MockResponse().setResponseCode(200); 110 | loMockResponseWithOneRepo.setBody(lsOneRepoJSONData); 111 | mMockWebServer.enqueue(loMockResponseWithOneRepo); 112 | try { 113 | mMockWebServer.start(4000); 114 | } catch (@NonNull final Exception loException) { 115 | loException.printStackTrace(); 116 | } 117 | mActivity = mActivityTestRule.launchActivity(null); 118 | } 119 | 120 | When: 121 | { 122 | mSolo.clickOnText("git-consortium"); 123 | } 124 | 125 | Then: 126 | { 127 | mSolo.assertCurrentActivity("should be on ActivityRepoDetail", ActivityRepoDetail.class); 128 | final boolean lbFoundTheRepo = mSolo.waitForText("This repo is for demonstration purposes only.", 1, 5000L, true); 129 | assertThat(lbFoundTheRepo).isTrue(); 130 | } 131 | } 132 | //endregion 133 | } 134 | -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/repo_octocat.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 18221276, 3 | "name": "git-consortium", 4 | "full_name": "octocat/git-consortium", 5 | "owner": { 6 | "login": "octocat", 7 | "id": 583231, 8 | "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=3", 9 | "gravatar_id": "", 10 | "url": "https://api.github.com/users/octocat", 11 | "html_url": "https://github.com/octocat", 12 | "followers_url": "https://api.github.com/users/octocat/followers", 13 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 14 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 15 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 16 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 17 | "organizations_url": "https://api.github.com/users/octocat/orgs", 18 | "repos_url": "https://api.github.com/users/octocat/repos", 19 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 20 | "received_events_url": "https://api.github.com/users/octocat/received_events", 21 | "type": "User", 22 | "site_admin": false 23 | }, 24 | "private": false, 25 | "html_url": "https://github.com/octocat/git-consortium", 26 | "description": "This repo is for demonstration purposes only.", 27 | "fork": false, 28 | "url": "https://api.github.com/repos/octocat/git-consortium", 29 | "forks_url": "https://api.github.com/repos/octocat/git-consortium/forks", 30 | "keys_url": "https://api.github.com/repos/octocat/git-consortium/keys{/key_id}", 31 | "collaborators_url": "https://api.github.com/repos/octocat/git-consortium/collaborators{/collaborator}", 32 | "teams_url": "https://api.github.com/repos/octocat/git-consortium/teams", 33 | "hooks_url": "https://api.github.com/repos/octocat/git-consortium/hooks", 34 | "issue_events_url": "https://api.github.com/repos/octocat/git-consortium/issues/events{/number}", 35 | "events_url": "https://api.github.com/repos/octocat/git-consortium/events", 36 | "assignees_url": "https://api.github.com/repos/octocat/git-consortium/assignees{/user}", 37 | "branches_url": "https://api.github.com/repos/octocat/git-consortium/branches{/branch}", 38 | "tags_url": "https://api.github.com/repos/octocat/git-consortium/tags", 39 | "blobs_url": "https://api.github.com/repos/octocat/git-consortium/git/blobs{/sha}", 40 | "git_tags_url": "https://api.github.com/repos/octocat/git-consortium/git/tags{/sha}", 41 | "git_refs_url": "https://api.github.com/repos/octocat/git-consortium/git/refs{/sha}", 42 | "trees_url": "https://api.github.com/repos/octocat/git-consortium/git/trees{/sha}", 43 | "statuses_url": "https://api.github.com/repos/octocat/git-consortium/statuses/{sha}", 44 | "languages_url": "https://api.github.com/repos/octocat/git-consortium/languages", 45 | "stargazers_url": "https://api.github.com/repos/octocat/git-consortium/stargazers", 46 | "contributors_url": "https://api.github.com/repos/octocat/git-consortium/contributors", 47 | "subscribers_url": "https://api.github.com/repos/octocat/git-consortium/subscribers", 48 | "subscription_url": "https://api.github.com/repos/octocat/git-consortium/subscription", 49 | "commits_url": "https://api.github.com/repos/octocat/git-consortium/commits{/sha}", 50 | "git_commits_url": "https://api.github.com/repos/octocat/git-consortium/git/commits{/sha}", 51 | "comments_url": "https://api.github.com/repos/octocat/git-consortium/comments{/number}", 52 | "issue_comment_url": "https://api.github.com/repos/octocat/git-consortium/issues/comments{/number}", 53 | "contents_url": "https://api.github.com/repos/octocat/git-consortium/contents/{+path}", 54 | "compare_url": "https://api.github.com/repos/octocat/git-consortium/compare/{base}...{head}", 55 | "merges_url": "https://api.github.com/repos/octocat/git-consortium/merges", 56 | "archive_url": "https://api.github.com/repos/octocat/git-consortium/{archive_format}{/ref}", 57 | "downloads_url": "https://api.github.com/repos/octocat/git-consortium/downloads", 58 | "issues_url": "https://api.github.com/repos/octocat/git-consortium/issues{/number}", 59 | "pulls_url": "https://api.github.com/repos/octocat/git-consortium/pulls{/number}", 60 | "milestones_url": "https://api.github.com/repos/octocat/git-consortium/milestones{/number}", 61 | "notifications_url": "https://api.github.com/repos/octocat/git-consortium/notifications{?since,all,participating}", 62 | "labels_url": "https://api.github.com/repos/octocat/git-consortium/labels{/name}", 63 | "releases_url": "https://api.github.com/repos/octocat/git-consortium/releases{/id}", 64 | "deployments_url": "https://api.github.com/repos/octocat/git-consortium/deployments", 65 | "created_at": "2014-03-28T17:55:38Z", 66 | "updated_at": "2015-11-29T09:53:51Z", 67 | "pushed_at": "2015-10-28T23:30:54Z", 68 | "git_url": "git://github.com/octocat/git-consortium.git", 69 | "ssh_url": "git@github.com:octocat/git-consortium.git", 70 | "clone_url": "https://github.com/octocat/git-consortium.git", 71 | "svn_url": "https://github.com/octocat/git-consortium", 72 | "homepage": null, 73 | "size": 190, 74 | "stargazers_count": 6, 75 | "watchers_count": 6, 76 | "language": null, 77 | "has_issues": true, 78 | "has_downloads": true, 79 | "has_wiki": true, 80 | "has_pages": false, 81 | "forks_count": 13, 82 | "mirror_url": null, 83 | "open_issues_count": 3, 84 | "forks": 13, 85 | "open_issues": 3, 86 | "watchers": 6, 87 | "default_branch": "master" 88 | } -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/repos_octocat.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 18221276, 4 | "name": "git-consortium", 5 | "full_name": "octocat/git-consortium", 6 | "owner": { 7 | "login": "octocat", 8 | "id": 583231, 9 | "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=3", 10 | "gravatar_id": "", 11 | "url": "https://api.github.com/users/octocat", 12 | "html_url": "https://github.com/octocat", 13 | "followers_url": "https://api.github.com/users/octocat/followers", 14 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 15 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 16 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 17 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 18 | "organizations_url": "https://api.github.com/users/octocat/orgs", 19 | "repos_url": "https://api.github.com/users/octocat/repos", 20 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 21 | "received_events_url": "https://api.github.com/users/octocat/received_events", 22 | "type": "User", 23 | "site_admin": false 24 | }, 25 | "private": false, 26 | "html_url": "https://github.com/octocat/git-consortium", 27 | "description": "This repo is for demonstration purposes only.", 28 | "fork": false, 29 | "url": "https://api.github.com/repos/octocat/git-consortium", 30 | "forks_url": "https://api.github.com/repos/octocat/git-consortium/forks", 31 | "keys_url": "https://api.github.com/repos/octocat/git-consortium/keys{/key_id}", 32 | "collaborators_url": "https://api.github.com/repos/octocat/git-consortium/collaborators{/collaborator}", 33 | "teams_url": "https://api.github.com/repos/octocat/git-consortium/teams", 34 | "hooks_url": "https://api.github.com/repos/octocat/git-consortium/hooks", 35 | "issue_events_url": "https://api.github.com/repos/octocat/git-consortium/issues/events{/number}", 36 | "events_url": "https://api.github.com/repos/octocat/git-consortium/events", 37 | "assignees_url": "https://api.github.com/repos/octocat/git-consortium/assignees{/user}", 38 | "branches_url": "https://api.github.com/repos/octocat/git-consortium/branches{/branch}", 39 | "tags_url": "https://api.github.com/repos/octocat/git-consortium/tags", 40 | "blobs_url": "https://api.github.com/repos/octocat/git-consortium/git/blobs{/sha}", 41 | "git_tags_url": "https://api.github.com/repos/octocat/git-consortium/git/tags{/sha}", 42 | "git_refs_url": "https://api.github.com/repos/octocat/git-consortium/git/refs{/sha}", 43 | "trees_url": "https://api.github.com/repos/octocat/git-consortium/git/trees{/sha}", 44 | "statuses_url": "https://api.github.com/repos/octocat/git-consortium/statuses/{sha}", 45 | "languages_url": "https://api.github.com/repos/octocat/git-consortium/languages", 46 | "stargazers_url": "https://api.github.com/repos/octocat/git-consortium/stargazers", 47 | "contributors_url": "https://api.github.com/repos/octocat/git-consortium/contributors", 48 | "subscribers_url": "https://api.github.com/repos/octocat/git-consortium/subscribers", 49 | "subscription_url": "https://api.github.com/repos/octocat/git-consortium/subscription", 50 | "commits_url": "https://api.github.com/repos/octocat/git-consortium/commits{/sha}", 51 | "git_commits_url": "https://api.github.com/repos/octocat/git-consortium/git/commits{/sha}", 52 | "comments_url": "https://api.github.com/repos/octocat/git-consortium/comments{/number}", 53 | "issue_comment_url": "https://api.github.com/repos/octocat/git-consortium/issues/comments{/number}", 54 | "contents_url": "https://api.github.com/repos/octocat/git-consortium/contents/{+path}", 55 | "compare_url": "https://api.github.com/repos/octocat/git-consortium/compare/{base}...{head}", 56 | "merges_url": "https://api.github.com/repos/octocat/git-consortium/merges", 57 | "archive_url": "https://api.github.com/repos/octocat/git-consortium/{archive_format}{/ref}", 58 | "downloads_url": "https://api.github.com/repos/octocat/git-consortium/downloads", 59 | "issues_url": "https://api.github.com/repos/octocat/git-consortium/issues{/number}", 60 | "pulls_url": "https://api.github.com/repos/octocat/git-consortium/pulls{/number}", 61 | "milestones_url": "https://api.github.com/repos/octocat/git-consortium/milestones{/number}", 62 | "notifications_url": "https://api.github.com/repos/octocat/git-consortium/notifications{?since,all,participating}", 63 | "labels_url": "https://api.github.com/repos/octocat/git-consortium/labels{/name}", 64 | "releases_url": "https://api.github.com/repos/octocat/git-consortium/releases{/id}", 65 | "deployments_url": "https://api.github.com/repos/octocat/git-consortium/deployments", 66 | "created_at": "2014-03-28T17:55:38Z", 67 | "updated_at": "2015-11-29T09:53:51Z", 68 | "pushed_at": "2015-10-28T23:30:54Z", 69 | "git_url": "git://github.com/octocat/git-consortium.git", 70 | "ssh_url": "git@github.com:octocat/git-consortium.git", 71 | "clone_url": "https://github.com/octocat/git-consortium.git", 72 | "svn_url": "https://github.com/octocat/git-consortium", 73 | "homepage": null, 74 | "size": 190, 75 | "stargazers_count": 6, 76 | "watchers_count": 6, 77 | "language": null, 78 | "has_issues": true, 79 | "has_downloads": true, 80 | "has_wiki": true, 81 | "has_pages": false, 82 | "forks_count": 13, 83 | "mirror_url": null, 84 | "open_issues_count": 3, 85 | "forks": 13, 86 | "open_issues": 3, 87 | "watchers": 6, 88 | "default_branch": "master" 89 | } 90 | ] -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/ApplicationAndroidStarter.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter; 2 | 3 | import android.app.Application; 4 | import android.os.StrictMode; 5 | 6 | import com.j256.ormlite.android.apptools.OpenHelperManager; 7 | import com.novoda.merlin.Merlin; 8 | import com.orhanobut.logger.LogLevel; 9 | import com.orhanobut.logger.Logger; 10 | 11 | import javax.inject.Inject; 12 | import javax.inject.Singleton; 13 | 14 | import autodagger.AutoComponent; 15 | import autodagger.AutoInjector; 16 | import fr.guddy.androidstarter.di.modules.ModuleAsync; 17 | import fr.guddy.androidstarter.di.modules.ModuleBus; 18 | import fr.guddy.androidstarter.di.modules.ModuleContext; 19 | import fr.guddy.androidstarter.di.modules.ModuleDatabase; 20 | import fr.guddy.androidstarter.di.modules.ModuleEnvironment; 21 | import fr.guddy.androidstarter.di.modules.ModuleRest; 22 | import fr.guddy.androidstarter.di.modules.ModuleTransformer; 23 | import fr.guddy.androidstarter.mvp.repo_detail.MvpRepoDetail; 24 | 25 | @AutoComponent( 26 | modules = { 27 | ModuleAsync.class, 28 | ModuleBus.class, 29 | ModuleContext.class, 30 | ModuleDatabase.class, 31 | ModuleEnvironment.class, 32 | ModuleRest.class, 33 | ModuleTransformer.class 34 | }, 35 | subcomponents = { 36 | MvpRepoDetail.class 37 | } 38 | ) 39 | @Singleton 40 | @AutoInjector(ApplicationAndroidStarter.class) 41 | public class ApplicationAndroidStarter extends Application { 42 | private static final String TAG = ApplicationAndroidStarter.class.getSimpleName(); 43 | 44 | //region Singleton 45 | protected static ApplicationAndroidStarter sSharedApplication; 46 | 47 | public static ApplicationAndroidStarter sharedApplication() { 48 | return sSharedApplication; 49 | } 50 | //endregion 51 | 52 | //region Fields (components) 53 | protected ApplicationAndroidStarterComponent mComponentApplication; 54 | //endregion 55 | 56 | //region Injected fields 57 | @Inject 58 | Merlin merlin; 59 | //endregion 60 | 61 | //region Overridden methods 62 | @Override 63 | public void onCreate() { 64 | super.onCreate(); 65 | sSharedApplication = this; 66 | 67 | Logger.init(TAG) 68 | .logLevel(LogLevel.FULL); 69 | 70 | buildComponent(); 71 | 72 | mComponentApplication.inject(this); 73 | merlin.bind(); 74 | 75 | final StrictMode.ThreadPolicy loStrictModeThreadPolicy = new StrictMode.ThreadPolicy.Builder() 76 | .detectAll() 77 | .penaltyDeath() 78 | .build(); 79 | StrictMode.setThreadPolicy(loStrictModeThreadPolicy); 80 | } 81 | 82 | @Override 83 | public void onTerminate() { 84 | super.onTerminate(); 85 | sSharedApplication = null; 86 | OpenHelperManager.releaseHelper(); 87 | merlin.unbind(); 88 | } 89 | //endregion 90 | 91 | //region Getters 92 | public ApplicationAndroidStarterComponent componentApplication() { 93 | return mComponentApplication; 94 | } 95 | //endregion 96 | 97 | //region Protected methods 98 | protected void buildComponent() { 99 | mComponentApplication = DaggerApplicationAndroidStarterComponent.builder() 100 | .moduleAsync(new ModuleAsync()) 101 | .moduleBus(new ModuleBus()) 102 | .moduleContext(new ModuleContext(getApplicationContext())) 103 | .moduleDatabase(new ModuleDatabase()) 104 | .moduleEnvironment(new ModuleEnvironment()) 105 | .moduleRest(new ModuleRest()) 106 | .moduleTransformer(new ModuleTransformer()) 107 | .build(); 108 | } 109 | //endregion 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/Environment.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter; 2 | 3 | import okhttp3.logging.HttpLoggingInterceptor; 4 | 5 | public enum Environment implements IEnvironment { 6 | DEV { 7 | @Override 8 | public HttpLoggingInterceptor.Level getHttpLoggingInterceptorLevel() { 9 | return HttpLoggingInterceptor.Level.BODY; 10 | } 11 | 12 | @Override 13 | public boolean isDebugDrawerEnabled() { 14 | return true; 15 | } 16 | }, 17 | PROD { 18 | @Override 19 | public HttpLoggingInterceptor.Level getHttpLoggingInterceptorLevel() { 20 | return HttpLoggingInterceptor.Level.NONE; 21 | } 22 | 23 | @Override 24 | public boolean isDebugDrawerEnabled() { 25 | return false; 26 | } 27 | }, 28 | TEST { 29 | @Override 30 | public HttpLoggingInterceptor.Level getHttpLoggingInterceptorLevel() { 31 | return HttpLoggingInterceptor.Level.BODY; 32 | } 33 | 34 | @Override 35 | public boolean isDebugDrawerEnabled() { 36 | return false; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/IEnvironment.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter; 2 | 3 | import okhttp3.logging.HttpLoggingInterceptor; 4 | 5 | public interface IEnvironment { 6 | 7 | HttpLoggingInterceptor.Level getHttpLoggingInterceptorLevel(); 8 | 9 | boolean isDebugDrawerEnabled(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/bus/BusMainThread.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.bus; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | import com.squareup.otto.Bus; 7 | 8 | public class BusMainThread extends Bus { 9 | //region Field 10 | private final Handler handler = new Handler(Looper.getMainLooper()); 11 | //endregion 12 | 13 | //region Constructor 14 | public BusMainThread(final String psName) { 15 | super(psName); 16 | } 17 | //endregion 18 | 19 | //region Overridden method 20 | @Override 21 | public void post(final Object event) { 22 | if (Looper.myLooper() == Looper.getMainLooper()) { 23 | super.post(event); 24 | } else { 25 | handler.post(() -> BusMainThread.super.post(event)); 26 | } 27 | } 28 | //endregion 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/bus/BusManager.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.bus; 2 | 3 | import com.squareup.otto.Bus; 4 | import com.squareup.otto.ThreadEnforcer; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import fr.guddy.androidstarter.bus.event.AbstractEvent; 9 | 10 | /** 11 | * Facade to fluidify event bus management. 12 | */ 13 | @Singleton 14 | public final class BusManager { 15 | 16 | //region Inner synthesize job 17 | private static final Bus sBusAnyThread = new Bus(ThreadEnforcer.ANY, "ANY_THREAD"); 18 | private static final BusMainThread sBusMainThread = new BusMainThread("MAIN_THREAD"); 19 | //endregion 20 | 21 | //region Specific any thread job 22 | public void registerSubscriberToBusAnyThread(final Object poSubscriber) { 23 | if (poSubscriber != null) { 24 | sBusAnyThread.register(poSubscriber); 25 | } 26 | } 27 | 28 | public void unregisterSubscriberFromBusAnyThread(final Object poSubscriber) { 29 | if (poSubscriber != null) { 30 | sBusAnyThread.unregister(poSubscriber); 31 | } 32 | } 33 | 34 | public void postEventOnAnyThread(final AbstractEvent poEvent) { 35 | if (poEvent != null) { 36 | sBusAnyThread.post(poEvent); 37 | } 38 | } 39 | //endregion 40 | 41 | //region Specific main thread job 42 | public void registerSubscriberToBusMainThread(final Object poSubscriber) { 43 | if (poSubscriber != null) { 44 | sBusMainThread.register(poSubscriber); 45 | } 46 | } 47 | 48 | public void unregisterSubscriberFromBusMainThread(final Object poSubscriber) { 49 | if (poSubscriber != null) { 50 | sBusMainThread.unregister(poSubscriber); 51 | } 52 | } 53 | 54 | public void postEventOnMainThread(final AbstractEvent poEvent) { 55 | if (poEvent != null) { 56 | sBusMainThread.post(poEvent); 57 | } 58 | } 59 | //endregion 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/bus/event/AbstractEvent.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.bus.event; 2 | 3 | public abstract class AbstractEvent { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/bus/event/AbstractEventQueryDidFinish.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.bus.event; 2 | 3 | import fr.guddy.androidstarter.rest.queries.AbstractQuery; 4 | 5 | public abstract class AbstractEventQueryDidFinish extends AbstractEvent { 6 | public enum ErrorType { 7 | UNKNOWN, 8 | NETWORK_UNREACHABLE 9 | } 10 | 11 | public final QueryType query; 12 | 13 | public final boolean success; 14 | public final ErrorType errorType; 15 | public final Throwable throwable; 16 | 17 | public AbstractEventQueryDidFinish(final QueryType poQuery, final boolean pbSuccess, final ErrorType poErrorType, final Throwable poThrowable) { 18 | query = poQuery; 19 | success = pbSuccess; 20 | errorType = poErrorType; 21 | throwable = poThrowable; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/di/modules/ModuleAsync.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.di.modules; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.birbit.android.jobqueue.JobManager; 7 | import com.birbit.android.jobqueue.config.Configuration; 8 | 9 | import javax.inject.Singleton; 10 | 11 | import dagger.Module; 12 | import dagger.Provides; 13 | 14 | @Module 15 | public class ModuleAsync { 16 | 17 | @Provides 18 | @Singleton 19 | public JobManager provideJobManager(@NonNull final Context poContext) { 20 | final Configuration loConfiguration = new Configuration.Builder(poContext) 21 | .minConsumerCount(1) //always keep at least one consumer alive 22 | .maxConsumerCount(3) //up to 3 consumers at a time 23 | .loadFactor(3) //3 jobs per consumer 24 | .consumerKeepAlive(120) //wait 2 minutes 25 | .build(); 26 | return new JobManager(loConfiguration); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/di/modules/ModuleBus.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.di.modules; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import dagger.Module; 6 | import dagger.Provides; 7 | import fr.guddy.androidstarter.bus.BusManager; 8 | 9 | @Module 10 | public class ModuleBus { 11 | 12 | @Provides 13 | @Singleton 14 | public BusManager provideBusManager() { 15 | return new BusManager(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/di/modules/ModuleContext.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.di.modules; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import dagger.Module; 7 | import dagger.Provides; 8 | 9 | @Module 10 | public class ModuleContext { 11 | private final Context mContext; 12 | 13 | public ModuleContext(@NonNull final Context poContext) { 14 | mContext = poContext; 15 | } 16 | 17 | @Provides 18 | public Context provideContext() { 19 | return mContext; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/di/modules/ModuleDatabase.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.di.modules; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.j256.ormlite.android.DatabaseTableConfigUtil; 7 | import com.j256.ormlite.support.ConnectionSource; 8 | import com.j256.ormlite.table.DatabaseTableConfig; 9 | import com.orhanobut.logger.Logger; 10 | 11 | import java.sql.SQLException; 12 | 13 | import javax.inject.Singleton; 14 | 15 | import dagger.Module; 16 | import dagger.Provides; 17 | import fr.guddy.androidstarter.BuildConfig; 18 | import fr.guddy.androidstarter.persistence.DatabaseHelperAndroidStarter; 19 | import fr.guddy.androidstarter.persistence.dao.DAORepo; 20 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 21 | 22 | @Module 23 | public class ModuleDatabase { 24 | private static final String TAG = ModuleDatabase.class.getSimpleName(); 25 | private static final boolean DEBUG = true; 26 | 27 | @Provides 28 | @Singleton 29 | public DatabaseHelperAndroidStarter provideDatabaseHelperAndroidStarter(@NonNull final Context poContext) { 30 | return new DatabaseHelperAndroidStarter(poContext); 31 | } 32 | 33 | @Provides 34 | @Singleton 35 | public DAORepo provideDAORepo(@NonNull final DatabaseHelperAndroidStarter poDatabaseHelperAndroidStarter) { 36 | try { 37 | final ConnectionSource loConnectionSource = poDatabaseHelperAndroidStarter.getConnectionSource(); 38 | final DatabaseTableConfig loTableConfig = DatabaseTableConfigUtil.fromClass(loConnectionSource, RepoEntity.class); 39 | if (loTableConfig != null) { 40 | return new DAORepo(loConnectionSource, loTableConfig); 41 | } else { 42 | return new DAORepo(loConnectionSource); 43 | } 44 | } catch (final SQLException loException) { 45 | if (BuildConfig.DEBUG && DEBUG) { 46 | Logger.t(TAG).e(loException, ""); 47 | } 48 | } 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/di/modules/ModuleEnvironment.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.di.modules; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import dagger.Module; 6 | import dagger.Provides; 7 | import fr.guddy.androidstarter.BuildConfig; 8 | import fr.guddy.androidstarter.IEnvironment; 9 | 10 | @Module 11 | public class ModuleEnvironment { 12 | 13 | @Provides 14 | @Singleton 15 | public IEnvironment provideEnvironment() { 16 | return BuildConfig.ENVIRONMENT; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/di/modules/ModuleRest.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.di.modules; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.novoda.merlin.Merlin; 7 | import com.novoda.merlin.MerlinsBeard; 8 | import com.squareup.picasso.Picasso; 9 | 10 | import javax.inject.Singleton; 11 | 12 | import dagger.Module; 13 | import dagger.Provides; 14 | import fr.guddy.androidstarter.IEnvironment; 15 | import fr.guddy.androidstarter.rest.GitHubService; 16 | import io.palaima.debugdrawer.picasso.PicassoModule; 17 | import okhttp3.Cache; 18 | import okhttp3.OkHttpClient; 19 | import okhttp3.logging.HttpLoggingInterceptor; 20 | import retrofit2.Retrofit; 21 | import retrofit2.converter.jackson.JacksonConverterFactory; 22 | 23 | @Module 24 | public class ModuleRest { 25 | 26 | private final static int CACHE_SIZE_BYTES = 1024 * 1024 * 2; 27 | 28 | @Provides 29 | @Singleton 30 | public OkHttpClient provideOkHttpClient(@NonNull final IEnvironment poEnvironment, @NonNull final Context poContext) { 31 | final HttpLoggingInterceptor loHttpLoggingInterceptor = new HttpLoggingInterceptor(); 32 | loHttpLoggingInterceptor.setLevel(poEnvironment.getHttpLoggingInterceptorLevel()); 33 | return new OkHttpClient.Builder() 34 | .addInterceptor(loHttpLoggingInterceptor) 35 | .cache(new Cache(poContext.getCacheDir(), CACHE_SIZE_BYTES)) 36 | .build(); 37 | } 38 | 39 | @Provides 40 | @Singleton 41 | public GitHubService provideGithubService(@NonNull final OkHttpClient poOkHttpClient) { 42 | final Retrofit loRetrofit = new Retrofit.Builder() 43 | .baseUrl("https://api.github.com") 44 | .client(poOkHttpClient) 45 | .addConverterFactory(JacksonConverterFactory.create()) 46 | .build(); 47 | return loRetrofit.create(GitHubService.class); 48 | } 49 | 50 | @Provides 51 | @Singleton 52 | public Merlin provideMerlin(@NonNull final Context poContext) { 53 | return new Merlin.Builder() 54 | .withConnectableCallbacks() 55 | .withDisconnectableCallbacks() 56 | .withBindableCallbacks() 57 | .withLogging(true) 58 | .build(poContext); 59 | } 60 | 61 | @Provides 62 | @Singleton 63 | public MerlinsBeard provideMerlinsBeard(@NonNull final Context poContext) { 64 | return MerlinsBeard.from(poContext); 65 | } 66 | 67 | @Provides 68 | @Singleton 69 | public Picasso providePicasso(@NonNull final Context poContext) { 70 | final Picasso loPicasso = Picasso.with(poContext); 71 | loPicasso.setIndicatorsEnabled(true); 72 | loPicasso.setLoggingEnabled(true); 73 | return loPicasso; 74 | } 75 | 76 | @Provides 77 | @Singleton 78 | public PicassoModule providePicassoModule(@NonNull final Picasso poPicasso) { 79 | return new PicassoModule(poPicasso); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/di/modules/ModuleTransformer.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.di.modules; 2 | 3 | import com.mobandme.android.transformer.Transformer; 4 | 5 | import javax.inject.Named; 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 11 | 12 | @Module 13 | public class ModuleTransformer { 14 | public static final String TRANSFORMER_REPO = "TRANSFORMER_REPO"; 15 | 16 | @Provides 17 | @Singleton 18 | @Named(TRANSFORMER_REPO) 19 | public Transformer provideTransformerRepo() { 20 | return new Transformer.Builder() 21 | .build(RepoEntity.class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/di/scopes/FragmentScope.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.di.scopes; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | 7 | import javax.inject.Scope; 8 | 9 | @Documented 10 | @Scope 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface FragmentScope { 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/mvp/repo_detail/ActivityRepoDetail.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.mvp.repo_detail; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v4.app.NavUtils; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.support.v7.widget.Toolbar; 8 | import android.view.MenuItem; 9 | 10 | import butterknife.BindView; 11 | import butterknife.ButterKnife; 12 | import fr.guddy.androidstarter.R; 13 | import fr.guddy.androidstarter.mvp.repo_list.ActivityRepoList; 14 | import se.emilsjolander.intentbuilder.Extra; 15 | import se.emilsjolander.intentbuilder.IntentBuilder; 16 | 17 | @IntentBuilder 18 | public class ActivityRepoDetail extends AppCompatActivity { 19 | 20 | //region Extra 21 | @Extra 22 | Long mItemId; 23 | //endregion 24 | 25 | //region Injected views 26 | @BindView(R.id.ActivityRepoDetail_Toolbar) 27 | Toolbar mToolbar; 28 | //endregion 29 | 30 | //region Lifecycle 31 | @Override 32 | protected void onCreate(final Bundle poSavedInstanceState) { 33 | super.onCreate(poSavedInstanceState); 34 | setContentView(R.layout.activity_repo_detail); 35 | 36 | ButterKnife.bind(this); 37 | 38 | ActivityRepoDetailIntentBuilder.inject(getIntent(), this); 39 | 40 | setSupportActionBar(mToolbar); 41 | 42 | // Show the Up button in the action bar. 43 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 44 | 45 | if (poSavedInstanceState == null) { 46 | 47 | final FragmentRepoDetail loFragment = new FragmentRepoDetailBuilder(mItemId).build(); 48 | 49 | getSupportFragmentManager().beginTransaction() 50 | .add(R.id.ActivityDetailRepo_container, loFragment) 51 | .commit(); 52 | } 53 | } 54 | //endregion 55 | 56 | //region Menu 57 | @Override 58 | public boolean onOptionsItemSelected(final MenuItem poItem) { 59 | final int liId = poItem.getItemId(); 60 | if (liId == android.R.id.home) { 61 | NavUtils.navigateUpTo(this, new Intent(this, ActivityRepoList.class)); 62 | return true; 63 | } 64 | return super.onOptionsItemSelected(poItem); 65 | } 66 | //endregion 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/mvp/repo_detail/FragmentRepoDetail.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.mvp.repo_detail; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.design.widget.CollapsingToolbarLayout; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.LinearLayout; 11 | import android.widget.ProgressBar; 12 | import android.widget.TextView; 13 | 14 | import com.hannesdorfmann.fragmentargs.FragmentArgs; 15 | import com.hannesdorfmann.fragmentargs.annotation.Arg; 16 | import com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs; 17 | import com.hannesdorfmann.mosby.mvp.MvpFragment; 18 | 19 | import butterknife.BindView; 20 | import butterknife.ButterKnife; 21 | import butterknife.Unbinder; 22 | import fr.guddy.androidstarter.ApplicationAndroidStarter; 23 | import fr.guddy.androidstarter.ApplicationAndroidStarterComponent; 24 | import fr.guddy.androidstarter.R; 25 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 26 | import pl.aprilapps.switcher.Switcher; 27 | 28 | @FragmentWithArgs 29 | public class FragmentRepoDetail 30 | extends MvpFragment 31 | implements MvpRepoDetail.View { 32 | 33 | //region FragmentArgs 34 | @Arg 35 | Long mItemId; 36 | //endregion 37 | 38 | //region Fields 39 | private Switcher mSwitcher; 40 | private Unbinder mUnbinder; 41 | //endregion 42 | 43 | //region Injected views 44 | @BindView(R.id.FragmentRepoDetail_TextView_Empty) 45 | TextView mTextViewEmpty; 46 | @BindView(R.id.FragmentRepoDetail_TextView_Error) 47 | TextView mTextViewError; 48 | @BindView(R.id.FragmentRepoDetail_ProgressBar_Loading) 49 | ProgressBar mProgressBarLoading; 50 | @BindView(R.id.FragmentRepoDetail_ContentView) 51 | LinearLayout mContentView; 52 | @BindView(R.id.FragmentRepoDetail_TextView_Description) 53 | TextView mTextViewDescription; 54 | @BindView(R.id.FragmentRepoDetail_TextView_Url) 55 | TextView mTextViewUrl; 56 | //endregion 57 | 58 | //region Default constructor 59 | public FragmentRepoDetail() { 60 | } 61 | //endregion 62 | 63 | //region Lifecycle 64 | @Override 65 | public void onCreate(final Bundle poSavedInstanceState) { 66 | super.onCreate(poSavedInstanceState); 67 | FragmentArgs.inject(this); 68 | } 69 | 70 | @Override 71 | public View onCreateView(final LayoutInflater poInflater, final ViewGroup poContainer, 72 | final Bundle savedInstanceState) { 73 | return poInflater.inflate(R.layout.fragment_repo_detail, poContainer, false); 74 | } 75 | 76 | @Override 77 | public void onViewCreated(final View poView, final Bundle poSavedInstanceState) { 78 | super.onViewCreated(poView, poSavedInstanceState); 79 | 80 | mUnbinder = ButterKnife.bind(this, poView); 81 | 82 | mSwitcher = new Switcher.Builder() 83 | .withEmptyView(mTextViewEmpty) 84 | .withProgressView(mProgressBarLoading) 85 | .withErrorView(mTextViewError) 86 | .withContentView(mContentView) 87 | .build(); 88 | 89 | loadData(false); 90 | } 91 | 92 | @Override 93 | public void onDestroyView() { 94 | super.onDestroyView(); 95 | mUnbinder.unbind(); 96 | } 97 | //endregion 98 | 99 | //region MvpFragment 100 | @NonNull 101 | @Override 102 | public PresenterRepoDetail createPresenter() { 103 | final ApplicationAndroidStarterComponent loComponent = ApplicationAndroidStarter.sharedApplication().componentApplication(); 104 | final MvpRepoDetailComponent loRepoDetailComponent = loComponent.plusMvpRepoDetailComponent(new MvpRepoDetail.Module()); 105 | return loRepoDetailComponent.presenterRepoDetail(); 106 | } 107 | //endregion 108 | 109 | //region ViewRepoDetail 110 | @Override 111 | public void showEmpty() { 112 | mSwitcher.showEmptyView(); 113 | } 114 | 115 | @Override 116 | public void showContent() { 117 | mSwitcher.showContentView(); 118 | } 119 | 120 | @Override 121 | public void showLoading(final boolean pbPullToRefresh) { 122 | mSwitcher.showProgressView(); 123 | } 124 | 125 | @Override 126 | public void showError(final Throwable poThrowable, final boolean pbPullToRefresh) { 127 | mSwitcher.showErrorView(); 128 | } 129 | 130 | @Override 131 | public void setData(final MvpRepoDetail.Model poData) { 132 | configureViewWithRepo(poData.repo); 133 | 134 | final Activity loActivity = this.getActivity(); 135 | final CollapsingToolbarLayout loAppBarLayout = (CollapsingToolbarLayout) loActivity.findViewById(R.id.ActivityRepoDetail_ToolbarLayout); 136 | if (loAppBarLayout != null) { 137 | loAppBarLayout.setTitle(poData.repo.name); 138 | } 139 | } 140 | 141 | @Override 142 | public void loadData(final boolean pbPullToRefresh) { 143 | if (mItemId == null) { 144 | mSwitcher.showErrorView(); 145 | } else { 146 | getPresenter().loadRepo(mItemId, pbPullToRefresh); 147 | } 148 | } 149 | //endregion 150 | 151 | //region Specific method 152 | private void configureViewWithRepo(@NonNull final RepoEntity poRepo) { 153 | mTextViewDescription.setText(poRepo.description); 154 | mTextViewUrl.setText(poRepo.url); 155 | } 156 | //endregion 157 | } 158 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/mvp/repo_detail/InteractorRepoDetail.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.mvp.repo_detail; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import javax.inject.Inject; 6 | 7 | import autodagger.AutoExpose; 8 | import fr.guddy.androidstarter.persistence.dao.DAORepo; 9 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 10 | import rx.Observable; 11 | 12 | @AutoExpose(MvpRepoDetail.class) 13 | public class InteractorRepoDetail implements MvpRepoDetail.Interactor { 14 | //region Injected fields 15 | private final DAORepo mDaoRepo; 16 | //endregion 17 | 18 | //region Constructor 19 | @Inject 20 | public InteractorRepoDetail(@NonNull final DAORepo poDaoRepo) { 21 | mDaoRepo = poDaoRepo; 22 | } 23 | //endregion 24 | 25 | //region Database job 26 | @Override 27 | public Observable getRepoById(final long plRepoId) { 28 | return mDaoRepo.rxQueryForId(plRepoId); 29 | } 30 | //endregion 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/mvp/repo_detail/MvpRepoDetail.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.mvp.repo_detail; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import com.hannesdorfmann.mosby.mvp.MvpPresenter; 6 | import com.hannesdorfmann.mosby.mvp.lce.MvpLceView; 7 | 8 | import autodagger.AutoSubcomponent; 9 | import dagger.Provides; 10 | import fr.guddy.androidstarter.di.scopes.FragmentScope; 11 | import fr.guddy.androidstarter.persistence.dao.DAORepo; 12 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 13 | import rx.Observable; 14 | 15 | @AutoSubcomponent( 16 | modules = {MvpRepoDetail.Module.class} 17 | ) 18 | @FragmentScope 19 | public interface MvpRepoDetail { 20 | //region Model 21 | final class Model { 22 | public final RepoEntity repo; 23 | 24 | public Model(final RepoEntity poRepo) { 25 | repo = poRepo; 26 | } 27 | } 28 | //endregion 29 | 30 | //region View 31 | interface View extends MvpLceView { 32 | void showEmpty(); 33 | } 34 | //endregion 35 | 36 | //region Presenter 37 | interface Presenter extends MvpPresenter { 38 | void loadRepo(final long plRepoId, final boolean pbPullToRefresh); 39 | } 40 | //endregion 41 | 42 | //region Interactor 43 | interface Interactor { 44 | Observable getRepoById(final long plRepoId); 45 | } 46 | //endregion 47 | 48 | //region Module 49 | @dagger.Module 50 | class Module { 51 | @Provides 52 | @FragmentScope 53 | public MvpRepoDetail.Interactor provideInteractor(@NonNull final DAORepo poDao) { 54 | return new InteractorRepoDetail(poDao); 55 | } 56 | 57 | @Provides 58 | @FragmentScope 59 | public MvpRepoDetail.Presenter providePresenter(@NonNull final MvpRepoDetail.Interactor poInteractor) { 60 | return new PresenterRepoDetail(poInteractor); 61 | } 62 | } 63 | //endregion 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/mvp/repo_detail/PresenterRepoDetail.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.mvp.repo_detail; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import com.hannesdorfmann.mosby.mvp.MvpBasePresenter; 6 | 7 | import javax.inject.Inject; 8 | 9 | import autodagger.AutoExpose; 10 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 11 | import rx.Subscription; 12 | import rx.android.schedulers.AndroidSchedulers; 13 | import rx.schedulers.Schedulers; 14 | 15 | @AutoExpose(MvpRepoDetail.class) 16 | public final class PresenterRepoDetail extends MvpBasePresenter implements MvpRepoDetail.Presenter { 17 | 18 | //region Fields 19 | private final MvpRepoDetail.Interactor mInteractor; 20 | private Subscription mSubscriptionGetRepo; 21 | //endregion 22 | 23 | //region Constructor 24 | @Inject 25 | public PresenterRepoDetail(@NonNull final MvpRepoDetail.Interactor poInteractor) { 26 | mInteractor = poInteractor; 27 | } 28 | //endregion 29 | 30 | //region Overridden method 31 | @Override 32 | public void detachView(final boolean pbRetainInstance) { 33 | super.detachView(pbRetainInstance); 34 | if (!pbRetainInstance) { 35 | unsubscribe(); 36 | } 37 | } 38 | //endregion 39 | 40 | //region Visible API 41 | public void loadRepo(final long plRepoId, final boolean pbPullToRefresh) { 42 | final MvpRepoDetail.View loView = getView(); 43 | if (isViewAttached() && loView != null) { 44 | loView.showLoading(pbPullToRefresh); 45 | } 46 | 47 | unsubscribe(); 48 | 49 | mSubscriptionGetRepo = mInteractor.getRepoById(plRepoId) 50 | .subscribeOn(Schedulers.newThread()) 51 | .observeOn(AndroidSchedulers.mainThread()) 52 | .subscribe( 53 | // onNext 54 | (final RepoEntity poRepo) -> { 55 | if (isViewAttached()) { 56 | loView.setData(new MvpRepoDetail.Model(poRepo)); 57 | if (poRepo == null) { 58 | loView.showEmpty(); 59 | } else { 60 | loView.showContent(); 61 | } 62 | } 63 | }, 64 | // onError 65 | (final Throwable poException) -> { 66 | if (isViewAttached()) { 67 | loView.showError(poException, false); 68 | } 69 | unsubscribe(); 70 | }, 71 | // onCompleted 72 | this::unsubscribe 73 | ); 74 | } 75 | //endregion 76 | 77 | //region Specific job 78 | private void unsubscribe() { 79 | if (mSubscriptionGetRepo != null && !mSubscriptionGetRepo.isUnsubscribed()) { 80 | mSubscriptionGetRepo.unsubscribe(); 81 | } 82 | 83 | mSubscriptionGetRepo = null; 84 | } 85 | //endregion 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/mvp/repo_list/ActivityRepoList.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.mvp.repo_list; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.squareup.picasso.Picasso; 7 | 8 | import javax.inject.Inject; 9 | 10 | import autodagger.AutoInjector; 11 | import butterknife.ButterKnife; 12 | import fr.guddy.androidstarter.ApplicationAndroidStarter; 13 | import fr.guddy.androidstarter.IEnvironment; 14 | import fr.guddy.androidstarter.R; 15 | import fr.guddy.androidstarter.mvp.repo_detail.ActivityRepoDetailIntentBuilder; 16 | import io.palaima.debugdrawer.DebugDrawer; 17 | import io.palaima.debugdrawer.commons.BuildModule; 18 | import io.palaima.debugdrawer.commons.DeviceModule; 19 | import io.palaima.debugdrawer.commons.NetworkModule; 20 | import io.palaima.debugdrawer.commons.SettingsModule; 21 | import io.palaima.debugdrawer.fps.FpsModule; 22 | import io.palaima.debugdrawer.picasso.PicassoModule; 23 | import io.palaima.debugdrawer.scalpel.ScalpelModule; 24 | import jp.wasabeef.takt.Takt; 25 | 26 | @AutoInjector(ApplicationAndroidStarter.class) 27 | public class ActivityRepoList extends AppCompatActivity 28 | implements FragmentRepoList.Callbacks { 29 | 30 | //region Fields 31 | private DebugDrawer mDebugDrawer; 32 | //endregion 33 | 34 | //region Injected fields 35 | @Inject 36 | Picasso mPicasso; 37 | @Inject 38 | IEnvironment mEnvironment; 39 | //endregion 40 | 41 | //region Lifecycle 42 | @Override 43 | protected void onCreate(final Bundle poSavedInstanceState) { 44 | super.onCreate(poSavedInstanceState); 45 | setContentView(R.layout.activity_repo_list); 46 | 47 | ApplicationAndroidStarter.sharedApplication().componentApplication().inject(this); 48 | 49 | ButterKnife.bind(this); 50 | 51 | if (mEnvironment.isDebugDrawerEnabled()) { 52 | mDebugDrawer = new DebugDrawer.Builder(this).modules( 53 | new FpsModule(Takt.stock(getApplication())), 54 | new ScalpelModule(this), 55 | new PicassoModule(mPicasso), 56 | new DeviceModule(this), 57 | new BuildModule(this), 58 | new NetworkModule(this), 59 | new SettingsModule(this) 60 | ).build(); 61 | } 62 | } 63 | 64 | @Override 65 | protected void onStart() { 66 | super.onStart(); 67 | 68 | if (mDebugDrawer != null) { 69 | mDebugDrawer.onStart(); 70 | } 71 | } 72 | 73 | @Override 74 | protected void onResume() { 75 | super.onResume(); 76 | 77 | if (mDebugDrawer != null) { 78 | mDebugDrawer.onResume(); 79 | } 80 | } 81 | 82 | @Override 83 | protected void onPause() { 84 | super.onPause(); 85 | 86 | if (mDebugDrawer != null) { 87 | mDebugDrawer.onPause(); 88 | } 89 | } 90 | 91 | @Override 92 | protected void onStop() { 93 | super.onStop(); 94 | 95 | if (mDebugDrawer != null) { 96 | mDebugDrawer.onStop(); 97 | } 98 | } 99 | //endregion 100 | 101 | //region FragmentRepoList.Callbacks 102 | 103 | /** 104 | * Callback method from {@link FragmentRepoList.Callbacks} 105 | * indicating that the item with the given ID was selected. 106 | */ 107 | @Override 108 | public void onItemSelected(final Long plId) { 109 | startActivity(new ActivityRepoDetailIntentBuilder(plId).build(this)); 110 | } 111 | //endregion 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/mvp/repo_list/CellRepo.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.mvp.repo_list; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.view.View; 6 | import android.widget.ImageView; 7 | import android.widget.TextView; 8 | 9 | import com.squareup.picasso.Picasso; 10 | import com.squareup.picasso.RequestCreator; 11 | 12 | import javax.inject.Inject; 13 | 14 | import autodagger.AutoInjector; 15 | import butterknife.BindView; 16 | import butterknife.ButterKnife; 17 | import fr.guddy.androidstarter.ApplicationAndroidStarter; 18 | import fr.guddy.androidstarter.R; 19 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 20 | import io.nlopez.smartadapters.views.BindableFrameLayout; 21 | 22 | @AutoInjector(ApplicationAndroidStarter.class) 23 | public class CellRepo extends BindableFrameLayout { 24 | //region Interactions 25 | public static final int ROW_PRESSED = 0; 26 | //endregion 27 | 28 | //region Injected members 29 | @Inject 30 | Picasso mPicasso; 31 | //endregion 32 | 33 | //region Injected views 34 | @BindView(R.id.CellRepo_TextView) 35 | TextView mTextView; 36 | @BindView(R.id.CellRepo_ImageView_Avatar) 37 | ImageView mImageViewAvatar; 38 | //endregion 39 | 40 | //region Constructor 41 | public CellRepo(@NonNull final Context poContext) { 42 | super(poContext); 43 | ApplicationAndroidStarter.sharedApplication().componentApplication().inject(this); 44 | } 45 | //endregion 46 | 47 | //region Overridden methods 48 | @Override 49 | public int getLayoutId() { 50 | return R.layout.cell_repo; 51 | } 52 | 53 | @Override 54 | public void bind(@NonNull final RepoEntity poRepo) { 55 | mTextView.setText(poRepo.url); 56 | 57 | final RequestCreator loRequest = mPicasso.load(poRepo.avatarUrl); 58 | if (loRequest != null) { 59 | loRequest 60 | .placeholder(R.drawable.git_icon) 61 | .error(R.drawable.git_icon) 62 | .into(mImageViewAvatar); 63 | } 64 | 65 | setOnClickListener((final View poView) -> 66 | notifyItemAction(ROW_PRESSED) 67 | ); 68 | } 69 | 70 | @Override 71 | public void onViewInflated() { 72 | super.onViewInflated(); 73 | ButterKnife.bind(this); 74 | } 75 | //endregion 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/mvp/repo_list/FragmentRepoList.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.mvp.repo_list; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.v4.widget.SwipeRefreshLayout; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.ProgressBar; 13 | import android.widget.TextView; 14 | 15 | import com.hannesdorfmann.mosby.mvp.viewstate.MvpViewStateFragment; 16 | import com.hannesdorfmann.mosby.mvp.viewstate.ViewState; 17 | import com.orhanobut.logger.Logger; 18 | 19 | import java.util.List; 20 | 21 | import butterknife.BindView; 22 | import butterknife.BindViews; 23 | import butterknife.ButterKnife; 24 | import butterknife.Unbinder; 25 | import fr.guddy.androidstarter.BuildConfig; 26 | import fr.guddy.androidstarter.R; 27 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 28 | import hugo.weaving.DebugLog; 29 | import icepick.Icepick; 30 | import io.nlopez.smartadapters.SmartAdapter; 31 | import io.nlopez.smartadapters.utils.ViewEventListener; 32 | import pl.aprilapps.switcher.Switcher; 33 | 34 | public class FragmentRepoList 35 | extends MvpViewStateFragment 36 | implements RepoListMvp.View, ViewEventListener, SwipeRefreshLayout.OnRefreshListener { 37 | 38 | private static final String TAG = FragmentRepoList.class.getSimpleName(); 39 | private static final boolean DEBUG = true; 40 | 41 | //region Mock callback constant 42 | /** 43 | * A dummy implementation of the {@link Callbacks} interface that does 44 | * nothing. Used only when this fragment is not attached to an activity. 45 | */ 46 | private static final Callbacks sDummyCallbacks = (final Long plId) -> { 47 | }; 48 | //endregion 49 | 50 | //region Injected views 51 | @BindView(R.id.FragmentRepoList_TextView_Empty) 52 | TextView mTextViewEmpty; 53 | @BindView(R.id.FragmentRepoList_ProgressBar_Loading) 54 | ProgressBar mProgressBarLoading; 55 | @BindView(R.id.FragmentRepoList_RecyclerView) 56 | RecyclerView mRecyclerView; 57 | @BindView(R.id.FragmentRepoList_TextView_Error) 58 | TextView mTextViewError; 59 | 60 | @BindView(R.id.FragmentRepoList_SwipeRefreshLayout_Empty) 61 | SwipeRefreshLayout mSwipeRefreshLayoutEmpty; 62 | @BindView(R.id.FragmentRepoList_SwipeRefreshLayout_Error) 63 | SwipeRefreshLayout mSwipeRefreshLayoutError; 64 | @BindView(R.id.FragmentRepoList_SwipeRefreshLayout_Content) 65 | SwipeRefreshLayout mSwipeRefreshLayoutContent; 66 | @BindViews({ 67 | R.id.FragmentRepoList_SwipeRefreshLayout_Empty, 68 | R.id.FragmentRepoList_SwipeRefreshLayout_Error, 69 | R.id.FragmentRepoList_SwipeRefreshLayout_Content 70 | }) 71 | List mSwipeRefreshLayouts; 72 | //endregion 73 | 74 | //region Fields 75 | static final ButterKnife.Setter SET_LISTENER = 76 | (@NonNull final SwipeRefreshLayout poView, @NonNull final SwipeRefreshLayout.OnRefreshListener poListener, final int piIndex) 77 | -> 78 | poView.setOnRefreshListener(poListener); 79 | 80 | static final ButterKnife.Action STOP_REFRESHING = 81 | (@NonNull final SwipeRefreshLayout poView, final int piIndex) 82 | -> 83 | poView.setRefreshing(false); 84 | 85 | private Switcher mSwitcher; 86 | 87 | private Callbacks mCallbacks = sDummyCallbacks; 88 | private Unbinder mUnbinder; 89 | //endregion 90 | 91 | //region Constructor 92 | 93 | public FragmentRepoList() { 94 | } 95 | //endregion 96 | 97 | //region Lifecycle 98 | @Override 99 | public void onAttach(final Context poContext) { 100 | super.onAttach(poContext); 101 | 102 | // Activities containing this fragment must implement its callbacks. 103 | if (!(poContext instanceof Callbacks)) { 104 | throw new IllegalStateException("Activity must implement fragment's callbacks."); 105 | } 106 | 107 | mCallbacks = (Callbacks) poContext; 108 | } 109 | 110 | @Override 111 | public View onCreateView(final LayoutInflater poInflater, final ViewGroup poContainer, final Bundle poSavedInstanceState) { 112 | View view = poInflater.inflate(R.layout.fragment_repo_list, poContainer, false); 113 | ButterKnife.bind(this, view); 114 | return view; 115 | } 116 | 117 | @Override 118 | public void onViewCreated(final View poView, final Bundle poSavedInstanceState) { 119 | super.onViewCreated(poView, poSavedInstanceState); 120 | Icepick.restoreInstanceState(this, poSavedInstanceState); 121 | 122 | mUnbinder = ButterKnife.bind(this, poView); 123 | 124 | ButterKnife.apply(mSwipeRefreshLayouts, SET_LISTENER, this); 125 | 126 | mSwitcher = new Switcher.Builder() 127 | .withEmptyView(mSwipeRefreshLayoutEmpty) 128 | .withProgressView(mProgressBarLoading) 129 | .withErrorView(mSwipeRefreshLayoutError) 130 | .withContentView(mSwipeRefreshLayoutContent) 131 | .build(); 132 | } 133 | 134 | @Override 135 | public void onActivityCreated(final Bundle poSavedInstanceState) { 136 | super.onActivityCreated(poSavedInstanceState); 137 | mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); 138 | } 139 | 140 | @Override 141 | public void onSaveInstanceState(final Bundle poOutState) { 142 | super.onSaveInstanceState(poOutState); 143 | Icepick.saveInstanceState(this, poOutState); 144 | } 145 | 146 | @Override 147 | public void onDestroyView() { 148 | super.onDestroyView(); 149 | 150 | mUnbinder.unbind(); 151 | } 152 | 153 | @Override 154 | public void onDetach() { 155 | super.onDetach(); 156 | 157 | // Reset the active callbacks interface to the dummy implementation. 158 | mCallbacks = sDummyCallbacks; 159 | } 160 | //endregion 161 | 162 | //region ViewEventListener 163 | @DebugLog 164 | @Override 165 | public void onViewEvent(final int piActionID, final RepoEntity poRepo, final int piPosition, final View poView) { 166 | if (piActionID == CellRepo.ROW_PRESSED) { 167 | mCallbacks.onItemSelected(poRepo.getBaseId()); 168 | } 169 | } 170 | //endregion 171 | 172 | //region MvpFragment 173 | @DebugLog 174 | @NonNull 175 | @Override 176 | public RepoListMvp.Presenter createPresenter() { 177 | return new PresenterRepoList(); 178 | } 179 | //endregion 180 | 181 | //region ViewRepoList 182 | @DebugLog 183 | @Override 184 | public void showEmpty() { 185 | ButterKnife.apply(mSwipeRefreshLayouts, STOP_REFRESHING); 186 | mSwitcher.showEmptyView(); 187 | } 188 | //endregion 189 | 190 | //region MvpLceView 191 | @DebugLog 192 | @Override 193 | public void showLoading(final boolean pbPullToRefresh) { 194 | if (!pbPullToRefresh) { 195 | mSwitcher.showProgressView(); 196 | } 197 | } 198 | 199 | @DebugLog 200 | @Override 201 | public void showContent() { 202 | ButterKnife.apply(mSwipeRefreshLayouts, STOP_REFRESHING); 203 | mSwitcher.showContentView(); 204 | } 205 | 206 | @DebugLog 207 | @Override 208 | public void showError(final Throwable poThrowable, final boolean pbPullToRefresh) { 209 | if (BuildConfig.DEBUG && DEBUG) { 210 | Logger.t(TAG).e(poThrowable, ""); 211 | } 212 | 213 | ButterKnife.apply(mSwipeRefreshLayouts, STOP_REFRESHING); 214 | mSwitcher.showErrorView(); 215 | } 216 | 217 | @DebugLog 218 | @Override 219 | public void setData(final RepoListMvp.Model poData) { 220 | ((RepoListMvp.ViewState) viewState).data = poData; 221 | 222 | SmartAdapter.items(poData.repos) 223 | .map(RepoEntity.class, CellRepo.class) 224 | .listener(FragmentRepoList.this) 225 | .into(mRecyclerView); 226 | } 227 | 228 | @DebugLog 229 | @Override 230 | public void loadData(final boolean pbPullToRefresh) { 231 | getPresenter().loadRepos(pbPullToRefresh); 232 | } 233 | //endregion 234 | 235 | //region MvpViewStateFragment 236 | @DebugLog 237 | @NonNull 238 | @Override 239 | public ViewState createViewState() { 240 | return new RepoListMvp.ViewState(); 241 | } 242 | 243 | @DebugLog 244 | @Override 245 | public void onNewViewStateInstance() { 246 | loadData(false); 247 | } 248 | //endregion 249 | 250 | //region SwipeRefreshLayout.OnRefreshListener 251 | @Override 252 | public void onRefresh() { 253 | loadData(true); 254 | } 255 | //endregion 256 | 257 | //region Callback definition 258 | 259 | /** 260 | * A callback interface that all activities containing this fragment must 261 | * implement. This mechanism allows activities to be notified of item 262 | * selections. 263 | */ 264 | public interface Callbacks { 265 | /** 266 | * Callback for when an item has been selected. 267 | */ 268 | void onItemSelected(final Long plId); 269 | } 270 | //endregion 271 | } 272 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/mvp/repo_list/PresenterRepoList.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.mvp.repo_list; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.birbit.android.jobqueue.JobManager; 7 | import com.hannesdorfmann.mosby.mvp.MvpBasePresenter; 8 | import com.orhanobut.logger.Logger; 9 | import com.squareup.otto.Subscribe; 10 | 11 | import java.util.List; 12 | 13 | import javax.inject.Inject; 14 | 15 | import autodagger.AutoInjector; 16 | import fr.guddy.androidstarter.ApplicationAndroidStarter; 17 | import fr.guddy.androidstarter.BuildConfig; 18 | import fr.guddy.androidstarter.bus.BusManager; 19 | import fr.guddy.androidstarter.persistence.dao.DAORepo; 20 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 21 | import fr.guddy.androidstarter.rest.queries.QueryGetRepos; 22 | import hugo.weaving.DebugLog; 23 | import rx.Observable; 24 | import rx.Subscription; 25 | import rx.android.schedulers.AndroidSchedulers; 26 | import rx.schedulers.Schedulers; 27 | 28 | @AutoInjector(ApplicationAndroidStarter.class) 29 | public class PresenterRepoList extends MvpBasePresenter implements RepoListMvp.Presenter { 30 | private static final String TAG = PresenterRepoList.class.getSimpleName(); 31 | private static final boolean DEBUG = true; 32 | 33 | //region Injected fields 34 | @Inject 35 | Context context; 36 | @Inject 37 | BusManager busManager; 38 | @Inject 39 | DAORepo daoRepo; 40 | @Inject 41 | JobManager jobManager; 42 | //endregion 43 | 44 | //region Fields 45 | private Subscription mSubscriptionGetRepos; 46 | //endregion 47 | 48 | //region Constructor 49 | public PresenterRepoList() { 50 | ApplicationAndroidStarter.sharedApplication().componentApplication().inject(this); 51 | } 52 | //endregion 53 | 54 | //region Overridden methods 55 | @Override 56 | public void attachView(final RepoListMvp.View poView) { 57 | super.attachView(poView); 58 | 59 | try { 60 | busManager.registerSubscriberToBusMainThread(this); 61 | } catch (final Exception loException) { 62 | if (BuildConfig.DEBUG && DEBUG) { 63 | Logger.t(TAG).e(loException, ""); 64 | } 65 | } 66 | } 67 | 68 | @Override 69 | public void detachView(final boolean pbRetainInstance) { 70 | super.detachView(pbRetainInstance); 71 | 72 | if (!pbRetainInstance) { 73 | unsubscribe(); 74 | } 75 | 76 | try { 77 | busManager.unregisterSubscriberFromBusMainThread(this); 78 | } catch (final Exception loException) { 79 | if (BuildConfig.DEBUG && DEBUG) { 80 | Logger.t(TAG).e(loException, ""); 81 | } 82 | } 83 | } 84 | //endregion 85 | 86 | //region Visible API 87 | @Override 88 | public void loadRepos(final boolean pbPullToRefresh) { 89 | startQueryGetRepos(pbPullToRefresh); 90 | } 91 | //endregion 92 | 93 | //region Reactive job 94 | private void getRepos(final boolean pbPullToRefresh) { 95 | unsubscribe(); 96 | 97 | final RepoListMvp.View loView = getView(); 98 | if (loView == null) { 99 | return; 100 | } 101 | 102 | mSubscriptionGetRepos = rxGetRepos() 103 | .subscribeOn(Schedulers.newThread()) 104 | .observeOn(AndroidSchedulers.mainThread()) 105 | .subscribe( 106 | // onNext 107 | (final List ploRepos) -> { 108 | if (isViewAttached()) { 109 | loView.setData(new RepoListMvp.Model(ploRepos)); 110 | if (ploRepos == null || ploRepos.isEmpty()) { 111 | loView.showEmpty(); 112 | } else { 113 | loView.showContent(); 114 | } 115 | } 116 | }, 117 | // onError 118 | (final Throwable poException) -> { 119 | if (isViewAttached()) { 120 | loView.showError(poException, pbPullToRefresh); 121 | } 122 | unsubscribe(); 123 | }, 124 | // onCompleted 125 | this::unsubscribe 126 | ); 127 | } 128 | //endregion 129 | 130 | //region Database job 131 | private Observable> rxGetRepos() { 132 | return daoRepo.rxQueryForAll(); 133 | } 134 | //endregion 135 | 136 | //region Network job 137 | private void startQueryGetRepos(final boolean pbPullToRefresh) { 138 | final RepoListMvp.View loView = getView(); 139 | if (isViewAttached() && loView != null) { 140 | loView.showLoading(pbPullToRefresh); 141 | } 142 | 143 | jobManager.addJobInBackground(new QueryGetRepos("RoRoche", pbPullToRefresh)); 144 | } 145 | //endregion 146 | 147 | //region Event management 148 | @DebugLog 149 | @Subscribe 150 | public void onEventQueryGetRepos(@NonNull final QueryGetRepos.EventQueryGetReposDidFinish poEvent) { 151 | if (poEvent.success) { 152 | getRepos(poEvent.pullToRefresh); 153 | } else { 154 | final RepoListMvp.View loView = getView(); 155 | if (isViewAttached() && loView != null) { 156 | loView.showError(poEvent.throwable, poEvent.pullToRefresh); 157 | } 158 | } 159 | } 160 | //endregion 161 | 162 | //region Specific job 163 | private void unsubscribe() { 164 | if (mSubscriptionGetRepos != null && !mSubscriptionGetRepos.isUnsubscribed()) { 165 | mSubscriptionGetRepos.unsubscribe(); 166 | } 167 | 168 | mSubscriptionGetRepos = null; 169 | } 170 | //endregion 171 | } 172 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/mvp/repo_list/RepoListMvp.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.mvp.repo_list; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.hannesdorfmann.mosby.mvp.MvpPresenter; 7 | import com.hannesdorfmann.mosby.mvp.lce.MvpLceView; 8 | import com.hannesdorfmann.mosby.mvp.viewstate.RestorableViewState; 9 | 10 | import java.io.Serializable; 11 | import java.util.List; 12 | 13 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 14 | import icepick.Icepick; 15 | import icepick.Icicle; 16 | 17 | public interface RepoListMvp { 18 | 19 | //region Model 20 | final class Model implements Serializable { 21 | 22 | public final List repos; 23 | 24 | public Model(final List ploRepos) { 25 | repos = ploRepos; 26 | } 27 | } 28 | //endregion 29 | 30 | //region View 31 | interface View extends MvpLceView { 32 | void showEmpty(); 33 | } 34 | //endregion 35 | 36 | //region Presenter 37 | interface Presenter extends MvpPresenter { 38 | void loadRepos(final boolean pbPullToRefresh); 39 | } 40 | //endregion 41 | 42 | //region ViewState 43 | final class ViewState implements RestorableViewState { 44 | 45 | //region Data to retain 46 | 47 | @Icicle 48 | public Serializable data; 49 | //endregion 50 | 51 | //region ViewState 52 | @Override 53 | public void apply(final View poView, final boolean pbRetained) { 54 | if (data instanceof Model) { 55 | final Model loData = (Model) data; 56 | poView.setData(loData); 57 | if (loData.repos == null || loData.repos.isEmpty()) { 58 | poView.showEmpty(); 59 | } else { 60 | poView.showContent(); 61 | } 62 | } else { 63 | poView.showError(null, false); 64 | } 65 | } 66 | //endregion 67 | 68 | //region RestorableViewState 69 | @Override 70 | public void saveInstanceState(@NonNull final Bundle poOut) { 71 | Icepick.saveInstanceState(this, poOut); 72 | } 73 | 74 | @Override 75 | public RestorableViewState restoreInstanceState(final Bundle poIn) { 76 | if (poIn == null) { 77 | return null; 78 | } 79 | Icepick.restoreInstanceState(this, poIn); 80 | return this; 81 | } 82 | //endregion 83 | } 84 | //endregion 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/persistence/DatabaseHelperAndroidStarter.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.persistence; 2 | 3 | import android.content.Context; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.support.annotation.NonNull; 6 | 7 | import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper; 8 | import com.j256.ormlite.support.ConnectionSource; 9 | import com.j256.ormlite.table.TableUtils; 10 | import com.orhanobut.logger.Logger; 11 | 12 | import java.sql.SQLException; 13 | 14 | import javax.inject.Singleton; 15 | 16 | import fr.guddy.androidstarter.BuildConfig; 17 | import fr.guddy.androidstarter.R; 18 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 19 | import hugo.weaving.DebugLog; 20 | 21 | @Singleton 22 | public class DatabaseHelperAndroidStarter extends OrmLiteSqliteOpenHelper { 23 | private static final String TAG = DatabaseHelperAndroidStarter.class.getSimpleName(); 24 | private static final boolean DEBUG = true; 25 | 26 | private static final String DATABASE_NAME = "android_starter.db"; 27 | private static final int DATABASE_VERSION = 1; 28 | 29 | //region Constructor 30 | public DatabaseHelperAndroidStarter(@NonNull final Context poContext) { 31 | super(poContext, DATABASE_NAME, null, DATABASE_VERSION, R.raw.ormlite_config); 32 | } 33 | 34 | protected DatabaseHelperAndroidStarter(@NonNull final Context poContext, 35 | @NonNull final String psDatabaseName, 36 | final SQLiteDatabase.CursorFactory poFactory, 37 | final int piDatabaseVersion) { 38 | super(poContext, psDatabaseName, poFactory, piDatabaseVersion, R.raw.ormlite_config); 39 | } 40 | //endregion 41 | 42 | //region Methods to override 43 | @DebugLog 44 | @Override 45 | public void onCreate(@NonNull final SQLiteDatabase poDatabase, @NonNull final ConnectionSource poConnectionSource) { 46 | try { 47 | TableUtils.createTable(poConnectionSource, RepoEntity.class); 48 | } catch (final SQLException loException) { 49 | if (BuildConfig.DEBUG && DEBUG) { 50 | Logger.t(TAG).e(loException, ""); 51 | } 52 | } 53 | } 54 | 55 | @DebugLog 56 | @Override 57 | public void onUpgrade(@NonNull final SQLiteDatabase poDatabase, @NonNull final ConnectionSource poConnectionSource, final int piOldVersion, final int piNewVersion) { 58 | try { 59 | TableUtils.dropTable(poConnectionSource, RepoEntity.class, true); 60 | } catch (final SQLException loException) { 61 | if (BuildConfig.DEBUG && DEBUG) { 62 | Logger.t(TAG).e(loException, ""); 63 | } 64 | } 65 | onCreate(poDatabase, poConnectionSource); 66 | } 67 | //endregion 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/persistence/dao/AbstractBaseDAOImpl.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.persistence.dao; 2 | 3 | import com.j256.ormlite.support.ConnectionSource; 4 | import com.j256.ormlite.table.DatabaseTableConfig; 5 | 6 | import java.sql.SQLException; 7 | 8 | import fr.guddy.androidstarter.persistence.entities.AbstractOrmLiteEntity; 9 | 10 | public abstract class AbstractBaseDAOImpl extends RxBaseDaoImpl implements IOrmLiteEntityDAO { 11 | //region Constructors matching super 12 | protected AbstractBaseDAOImpl(final Class poDataClass) throws SQLException { 13 | super(poDataClass); 14 | } 15 | 16 | public AbstractBaseDAOImpl(final ConnectionSource poConnectionSource, final Class poDataClass) throws SQLException { 17 | super(poConnectionSource, poDataClass); 18 | } 19 | 20 | public AbstractBaseDAOImpl(final ConnectionSource poConnectionSource, final DatabaseTableConfig poTableConfig) throws SQLException { 21 | super(poConnectionSource, poTableConfig); 22 | } 23 | //endregion 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/persistence/dao/DAORepo.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.persistence.dao; 2 | 3 | import com.j256.ormlite.support.ConnectionSource; 4 | import com.j256.ormlite.table.DatabaseTableConfig; 5 | 6 | import java.sql.SQLException; 7 | 8 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 9 | 10 | public class DAORepo extends AbstractBaseDAOImpl { 11 | //region Constructors matching super 12 | public DAORepo(final ConnectionSource poConnectionSource) throws SQLException { 13 | this(poConnectionSource, RepoEntity.class); 14 | } 15 | 16 | public DAORepo(final ConnectionSource poConnectionSource, final Class poDataClass) throws SQLException { 17 | super(poConnectionSource, poDataClass); 18 | } 19 | 20 | public DAORepo(final ConnectionSource poConnectionSource, final DatabaseTableConfig poTableConfig) throws SQLException { 21 | super(poConnectionSource, poTableConfig); 22 | } 23 | //endregion 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/persistence/dao/IOrmLiteEntityDAO.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.persistence.dao; 2 | 3 | import com.j256.ormlite.dao.Dao; 4 | 5 | import fr.guddy.androidstarter.persistence.entities.AbstractOrmLiteEntity; 6 | 7 | /** 8 | * An interface to contract implementation with a {@link Long} ID type. 9 | */ 10 | public interface IOrmLiteEntityDAO extends Dao { 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/persistence/entities/AbstractOrmLiteEntity.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.persistence.entities; 2 | 3 | import android.provider.BaseColumns; 4 | 5 | import com.j256.ormlite.field.DatabaseField; 6 | 7 | public abstract class AbstractOrmLiteEntity { 8 | @DatabaseField(columnName = BaseColumns._ID, generatedId = true) 9 | protected long _id; 10 | 11 | //region Getter 12 | public long getBaseId() { 13 | return _id; 14 | } 15 | //endregion 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/persistence/entities/RepoEntity.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.persistence.entities; 2 | 3 | import com.j256.ormlite.field.DatabaseField; 4 | import com.j256.ormlite.table.DatabaseTable; 5 | import com.mobandme.android.transformer.compiler.Mappable; 6 | import com.mobandme.android.transformer.compiler.Mapped; 7 | 8 | import java.io.Serializable; 9 | 10 | import fr.guddy.androidstarter.persistence.dao.DAORepo; 11 | import fr.guddy.androidstarter.rest.dto.DTORepo; 12 | 13 | @Mappable(with = DTORepo.class) 14 | @DatabaseTable(tableName = "REPO", daoClass = DAORepo.class) 15 | public class RepoEntity extends AbstractOrmLiteEntity implements Serializable { 16 | @Mapped 17 | @DatabaseField 18 | public Integer id; 19 | 20 | @Mapped 21 | @DatabaseField 22 | public String name; 23 | 24 | @Mapped 25 | @DatabaseField 26 | public String description; 27 | 28 | @Mapped 29 | @DatabaseField 30 | public String url; 31 | 32 | @DatabaseField 33 | public String avatarUrl; 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/rest/GitHubService.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.rest; 2 | 3 | import java.util.List; 4 | 5 | import fr.guddy.androidstarter.rest.dto.DTORepo; 6 | import retrofit2.Call; 7 | import retrofit2.http.GET; 8 | import retrofit2.http.Path; 9 | 10 | public interface GitHubService { 11 | @GET("/users/{user}/repos") 12 | Call> listRepos(@Path("user") final String psUser); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/rest/dto/DTOOwner.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.rest.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAnyGetter; 4 | import com.fasterxml.jackson.annotation.JsonAnySetter; 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | import com.fasterxml.jackson.annotation.JsonInclude; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | @JsonInclude(JsonInclude.Include.NON_NULL) 13 | public class DTOOwner { 14 | @JsonProperty("login") 15 | public String login; 16 | @JsonProperty("id") 17 | public Integer id; 18 | @JsonProperty("avatar_url") 19 | public String avatarUrl; 20 | 21 | @JsonIgnore 22 | private final Map additionalProperties = new HashMap<>(); 23 | 24 | @JsonAnyGetter 25 | public Map getAdditionalProperties() { 26 | return this.additionalProperties; 27 | } 28 | 29 | @JsonAnySetter 30 | public void setAdditionalProperty(String name, Object value) { 31 | this.additionalProperties.put(name, value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/rest/dto/DTORepo.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.rest.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAnyGetter; 4 | import com.fasterxml.jackson.annotation.JsonAnySetter; 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | import com.fasterxml.jackson.annotation.JsonInclude; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | @JsonInclude(JsonInclude.Include.NON_NULL) 13 | public class DTORepo { 14 | @JsonProperty("id") 15 | public Integer id; 16 | @JsonProperty("name") 17 | public String name; 18 | @JsonProperty("full_name") 19 | public String fullName; 20 | @JsonProperty("owner") 21 | public DTOOwner owner; 22 | @JsonProperty("private") 23 | public Boolean _private; 24 | @JsonProperty("html_url") 25 | public String htmlUrl; 26 | @JsonProperty("description") 27 | public String description; 28 | @JsonProperty("fork") 29 | public Boolean fork; 30 | @JsonProperty("url") 31 | public String url; 32 | @JsonProperty("forks_url") 33 | public String forksUrl; 34 | @JsonProperty("keys_url") 35 | public String keysUrl; 36 | @JsonProperty("collaborators_url") 37 | public String collaboratorsUrl; 38 | @JsonProperty("teams_url") 39 | public String teamsUrl; 40 | @JsonProperty("hooks_url") 41 | public String hooksUrl; 42 | @JsonProperty("issue_events_url") 43 | public String issueEventsUrl; 44 | @JsonProperty("events_url") 45 | public String eventsUrl; 46 | @JsonProperty("assignees_url") 47 | public String assigneesUrl; 48 | @JsonProperty("branches_url") 49 | public String branchesUrl; 50 | @JsonProperty("tags_url") 51 | public String tagsUrl; 52 | @JsonProperty("blobs_url") 53 | public String blobsUrl; 54 | @JsonProperty("git_tags_url") 55 | public String gitTagsUrl; 56 | @JsonProperty("git_refs_url") 57 | public String gitRefsUrl; 58 | @JsonProperty("trees_url") 59 | public String treesUrl; 60 | @JsonProperty("statuses_url") 61 | public String statusesUrl; 62 | @JsonProperty("languages_url") 63 | public String languagesUrl; 64 | @JsonProperty("stargazers_url") 65 | public String stargazersUrl; 66 | @JsonProperty("contributors_url") 67 | public String contributorsUrl; 68 | @JsonProperty("subscribers_url") 69 | public String subscribersUrl; 70 | @JsonProperty("subscription_url") 71 | public String subscriptionUrl; 72 | @JsonProperty("commits_url") 73 | public String commitsUrl; 74 | @JsonProperty("git_commits_url") 75 | public String gitCommitsUrl; 76 | @JsonProperty("comments_url") 77 | public String commentsUrl; 78 | @JsonProperty("issue_comment_url") 79 | public String issueCommentUrl; 80 | @JsonProperty("contents_url") 81 | public String contentsUrl; 82 | @JsonProperty("compare_url") 83 | public String compareUrl; 84 | @JsonProperty("merges_url") 85 | public String mergesUrl; 86 | @JsonProperty("archive_url") 87 | public String archiveUrl; 88 | @JsonProperty("downloads_url") 89 | public String downloadsUrl; 90 | @JsonProperty("issues_url") 91 | public String issuesUrl; 92 | @JsonProperty("pulls_url") 93 | public String pullsUrl; 94 | @JsonProperty("milestones_url") 95 | public String milestonesUrl; 96 | @JsonProperty("notifications_url") 97 | public String notificationsUrl; 98 | @JsonProperty("labels_url") 99 | public String labelsUrl; 100 | @JsonProperty("releases_url") 101 | public String releasesUrl; 102 | @JsonProperty("deployments_url") 103 | public String deploymentsUrl; 104 | @JsonProperty("created_at") 105 | public String createdAt; 106 | @JsonProperty("updated_at") 107 | public String updatedAt; 108 | @JsonProperty("pushed_at") 109 | public String pushedAt; 110 | @JsonProperty("git_url") 111 | public String gitUrl; 112 | @JsonProperty("ssh_url") 113 | public String sshUrl; 114 | @JsonProperty("clone_url") 115 | public String cloneUrl; 116 | @JsonProperty("svn_url") 117 | public String svnUrl; 118 | @JsonProperty("homepage") 119 | public String homepage; 120 | @JsonProperty("size") 121 | public Integer size; 122 | @JsonProperty("stargazers_count") 123 | public Integer stargazersCount; 124 | @JsonProperty("watchers_count") 125 | public Integer watchersCount; 126 | @JsonProperty("language") 127 | public String language; 128 | @JsonProperty("has_issues") 129 | public Boolean hasIssues; 130 | @JsonProperty("has_downloads") 131 | public Boolean hasDownloads; 132 | @JsonProperty("has_wiki") 133 | public Boolean hasWiki; 134 | @JsonProperty("has_pages") 135 | public Boolean hasPages; 136 | @JsonProperty("forks_count") 137 | public Integer forksCount; 138 | @JsonProperty("mirror_url") 139 | public Object mirrorUrl; 140 | @JsonProperty("open_issues_count") 141 | public Integer openIssuesCount; 142 | @JsonProperty("forks") 143 | public Integer forks; 144 | @JsonProperty("open_issues") 145 | public Integer openIssues; 146 | @JsonProperty("watchers") 147 | public Integer watchers; 148 | @JsonProperty("default_branch") 149 | public String defaultBranch; 150 | 151 | @JsonIgnore 152 | private final Map additionalProperties = new HashMap<>(); 153 | 154 | @JsonAnyGetter 155 | public Map getAdditionalProperties() { 156 | return this.additionalProperties; 157 | } 158 | 159 | @JsonAnySetter 160 | public void setAdditionalProperty(String name, Object value) { 161 | this.additionalProperties.put(name, value); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/rest/error_handling/RetrofitException.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.rest.error_handling; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | 6 | import java.io.IOException; 7 | import java.lang.annotation.Annotation; 8 | 9 | import okhttp3.ResponseBody; 10 | import retrofit2.Converter; 11 | import retrofit2.Response; 12 | import retrofit2.Retrofit; 13 | 14 | public class RetrofitException extends RuntimeException { 15 | 16 | public static RetrofitException httpError(@NonNull final String psUrl, @NonNull final Response poResponse, @NonNull final Retrofit poRetrofit) { 17 | final String lsMessage = poResponse.code() + " " + poResponse.message(); 18 | return new RetrofitException(lsMessage, psUrl, poResponse, Kind.HTTP, null, poRetrofit); 19 | } 20 | 21 | public static RetrofitException networkError(@NonNull final IOException poException) { 22 | return new RetrofitException(poException.getMessage(), null, null, Kind.NETWORK, poException, null); 23 | } 24 | 25 | public static RetrofitException unexpectedError(@NonNull final Throwable poException) { 26 | return new RetrofitException(poException.getMessage(), null, null, Kind.UNEXPECTED, poException, null); 27 | } 28 | 29 | /** 30 | * Identifies the event kind which triggered a {@link RetrofitException}. 31 | */ 32 | public enum Kind { 33 | /** 34 | * An {@link IOException} occurred while communicating to the server. 35 | */ 36 | NETWORK, 37 | /** 38 | * A non-200 HTTP status code was received from the server. 39 | */ 40 | HTTP, 41 | /** 42 | * An internal error occurred while attempting to execute a request. It is best practice to 43 | * re-throw this exception so your application crashes. 44 | */ 45 | UNEXPECTED 46 | } 47 | 48 | private final String mUrl; 49 | private final Response mResponse; 50 | private final Kind mKind; 51 | private final Retrofit mRetrofit; 52 | 53 | RetrofitException(@NonNull final String psMessage, 54 | @Nullable final String psUrl, 55 | @Nullable final Response poResponse, 56 | @NonNull final Kind poKind, 57 | @Nullable final Throwable poException, 58 | @Nullable final Retrofit poRetrofit) { 59 | super(psMessage, poException); 60 | mUrl = psUrl; 61 | mResponse = poResponse; 62 | mKind = poKind; 63 | mRetrofit = poRetrofit; 64 | } 65 | 66 | /** 67 | * The request URL which produced the error. 68 | */ 69 | public String getUrl() { 70 | return mUrl; 71 | } 72 | 73 | /** 74 | * Response object containing status code, headers, body, etc. 75 | */ 76 | public Response getResponse() { 77 | return mResponse; 78 | } 79 | 80 | /** 81 | * The event kind which triggered this error. 82 | */ 83 | public Kind getKind() { 84 | return mKind; 85 | } 86 | 87 | /** 88 | * The Retrofit this request was executed on 89 | */ 90 | public Retrofit getRetrofit() { 91 | return mRetrofit; 92 | } 93 | 94 | /** 95 | * HTTP response body converted to specified {@code poType}. {@code null} if there is no 96 | * response. 97 | * 98 | * @throws IOException if unable to convert the body to the specified {@code poType}. 99 | */ 100 | public T getErrorBodyAs(@NonNull final Class poType) throws IOException { 101 | if (mResponse == null || mResponse.errorBody() == null || mRetrofit == null) { 102 | return null; 103 | } 104 | final Converter loConverter = mRetrofit.responseBodyConverter(poType, new Annotation[0]); 105 | return loConverter.convert(mResponse.errorBody()); 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/rest/queries/AbstractQuery.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.rest.queries; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | 6 | import com.birbit.android.jobqueue.Job; 7 | import com.birbit.android.jobqueue.Params; 8 | import com.birbit.android.jobqueue.RetryConstraint; 9 | import com.orhanobut.logger.Logger; 10 | 11 | import java.net.HttpURLConnection; 12 | 13 | import fr.guddy.androidstarter.BuildConfig; 14 | import fr.guddy.androidstarter.bus.event.AbstractEventQueryDidFinish; 15 | import hugo.weaving.DebugLog; 16 | import retrofit2.Response; 17 | 18 | public abstract class AbstractQuery extends Job { 19 | private static final String TAG = AbstractQuery.class.getSimpleName(); 20 | private static final boolean DEBUG = true; 21 | 22 | protected enum Priority { 23 | LOW(0), 24 | MEDIUM(500), 25 | HIGH(1000); 26 | private final int value; 27 | 28 | Priority(final int piValue) { 29 | value = piValue; 30 | } 31 | } 32 | 33 | protected boolean mSuccess; 34 | protected Throwable mThrowable; 35 | protected AbstractEventQueryDidFinish.ErrorType mErrorType; 36 | 37 | //region Protected constructor 38 | protected AbstractQuery(final Priority poPriority) { 39 | super(new Params(poPriority.value).requireNetwork()); 40 | } 41 | 42 | protected AbstractQuery(final Priority poPriority, final boolean pbPersistent, final String psGroupId, final long plDelayMs) { 43 | super(new Params(poPriority.value).requireNetwork().setPersistent(pbPersistent).setGroupId(psGroupId).setDelayMs(plDelayMs)); 44 | } 45 | //endregion 46 | 47 | //region Overridden methods 48 | @DebugLog 49 | @Override 50 | public void onAdded() { 51 | if (BuildConfig.DEBUG && DEBUG) { 52 | Logger.t(TAG).d(""); 53 | } 54 | } 55 | 56 | @DebugLog 57 | @Override 58 | public void onRun() throws Throwable { 59 | if (BuildConfig.DEBUG && DEBUG) { 60 | Logger.t(TAG).d(""); 61 | } 62 | 63 | inject(); 64 | 65 | try { 66 | execute(); 67 | mSuccess = true; 68 | } catch (Throwable loThrowable) { 69 | if (BuildConfig.DEBUG && DEBUG) { 70 | Logger.t(TAG).e(loThrowable, ""); 71 | } 72 | mErrorType = AbstractEventQueryDidFinish.ErrorType.UNKNOWN; 73 | mThrowable = loThrowable; 74 | mSuccess = false; 75 | } 76 | 77 | postEventQueryFinished(); 78 | } 79 | 80 | @Override 81 | protected void onCancel(final int cancelReason, @Nullable final Throwable poThrowable) { 82 | } 83 | 84 | @Override 85 | protected RetryConstraint shouldReRunOnThrowable(@NonNull final Throwable poThrowable, final int piRunCount, final int piMaxRunCount) { 86 | return null; 87 | } 88 | 89 | @DebugLog 90 | @Override 91 | protected int getRetryLimit() { 92 | return 1; 93 | } 94 | //endregion 95 | 96 | //region Protected helper method 97 | protected boolean isCached(@NonNull final Response poResponse) { 98 | if (poResponse.isSuccessful() && 99 | ( 100 | (poResponse.raw().networkResponse() != null && poResponse.raw().networkResponse().code() == HttpURLConnection.HTTP_NOT_MODIFIED) 101 | || 102 | (poResponse.raw().networkResponse() == null && poResponse.raw().cacheResponse() != null)) 103 | ) { 104 | return true; 105 | } 106 | return false; 107 | } 108 | //endregion 109 | 110 | //region Protected abstract method for specific job 111 | public abstract void inject(); 112 | 113 | protected abstract void execute() throws Exception; 114 | 115 | protected abstract void postEventQueryFinished(); 116 | 117 | public abstract void postEventQueryFinishedNoNetwork(); 118 | //endregion 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/fr/guddy/androidstarter/rest/queries/QueryGetRepos.java: -------------------------------------------------------------------------------- 1 | package fr.guddy.androidstarter.rest.queries; 2 | 3 | import com.j256.ormlite.dao.Dao; 4 | import com.mobandme.android.transformer.Transformer; 5 | import com.orhanobut.logger.Logger; 6 | 7 | import java.util.List; 8 | 9 | import javax.inject.Inject; 10 | import javax.inject.Named; 11 | 12 | import autodagger.AutoInjector; 13 | import fr.guddy.androidstarter.ApplicationAndroidStarter; 14 | import fr.guddy.androidstarter.BuildConfig; 15 | import fr.guddy.androidstarter.bus.BusManager; 16 | import fr.guddy.androidstarter.bus.event.AbstractEventQueryDidFinish; 17 | import fr.guddy.androidstarter.di.modules.ModuleTransformer; 18 | import fr.guddy.androidstarter.persistence.dao.DAORepo; 19 | import fr.guddy.androidstarter.persistence.entities.RepoEntity; 20 | import fr.guddy.androidstarter.rest.GitHubService; 21 | import fr.guddy.androidstarter.rest.dto.DTORepo; 22 | import retrofit2.Call; 23 | import retrofit2.Response; 24 | 25 | @AutoInjector(ApplicationAndroidStarter.class) 26 | public class QueryGetRepos extends AbstractQuery { 27 | private static final String TAG = QueryGetRepos.class.getSimpleName(); 28 | private static final boolean DEBUG = true; 29 | 30 | //region Injected fields 31 | @Inject 32 | transient GitHubService gitHubService; 33 | @Inject 34 | transient BusManager busManager; 35 | @Inject 36 | transient DAORepo daoRepo; 37 | @Inject 38 | @Named(ModuleTransformer.TRANSFORMER_REPO) 39 | transient Transformer transformerRepo; 40 | //endregion 41 | 42 | //region Fields 43 | public final boolean pullToRefresh; 44 | public final String user; 45 | public List results; 46 | //endregion 47 | 48 | //region Constructor matching super 49 | public QueryGetRepos(final String psUser, final boolean pbPullToRefresh) { 50 | super(Priority.MEDIUM); 51 | user = psUser; 52 | pullToRefresh = pbPullToRefresh; 53 | } 54 | //endregion 55 | 56 | //region Overridden method 57 | @Override 58 | public void inject() { 59 | ApplicationAndroidStarter.sharedApplication().componentApplication().inject(this); 60 | } 61 | 62 | @Override 63 | protected void execute() throws Exception { 64 | final Call> loCall = gitHubService.listRepos(user); 65 | final Response> loExecute = loCall.execute(); 66 | 67 | if (isCached(loExecute)) { 68 | // not modified, no need to do anything 69 | return; 70 | } 71 | 72 | results = loExecute.body(); 73 | 74 | final int liDeleted = daoRepo.deleteBuilder().delete(); 75 | 76 | if (BuildConfig.DEBUG && DEBUG) { 77 | Logger.t(TAG).d("deleted row count = %d", liDeleted); 78 | } 79 | 80 | int liCount = 0; 81 | for (final DTORepo loDTORepo : results) { 82 | final RepoEntity loRepo = transformerRepo.transform(loDTORepo, RepoEntity.class); 83 | loRepo.avatarUrl = loDTORepo.owner.avatarUrl; 84 | final Dao.CreateOrUpdateStatus loStatus = daoRepo.createOrUpdate(loRepo); 85 | if (loStatus.isCreated() || loStatus.isUpdated()) { 86 | ++liCount; 87 | } 88 | } 89 | 90 | if (BuildConfig.DEBUG && DEBUG) { 91 | Logger.t(TAG).d("created or updated row count = %d", liCount); 92 | } 93 | } 94 | 95 | @Override 96 | protected void postEventQueryFinished() { 97 | final EventQueryGetReposDidFinish loEvent = new EventQueryGetReposDidFinish(this, mSuccess, mErrorType, mThrowable, pullToRefresh, results); 98 | busManager.postEventOnMainThread(loEvent); 99 | busManager.postEventOnAnyThread(loEvent); 100 | } 101 | 102 | @Override 103 | public void postEventQueryFinishedNoNetwork() { 104 | final EventQueryGetReposDidFinish loEvent = new EventQueryGetReposDidFinish(this, false, AbstractEventQueryDidFinish.ErrorType.NETWORK_UNREACHABLE, null, pullToRefresh, null); 105 | busManager.postEventOnMainThread(loEvent); 106 | busManager.postEventOnAnyThread(loEvent); 107 | } 108 | //endregion 109 | 110 | //region Dedicated EventQueryDidFinish 111 | public static final class EventQueryGetReposDidFinish extends AbstractEventQueryDidFinish { 112 | public final boolean pullToRefresh; 113 | public final List results; 114 | 115 | public EventQueryGetReposDidFinish(final QueryGetRepos poQuery, final boolean pbSuccess, final ErrorType poErrorType, final Throwable poThrowable, final boolean pbPullToRefresh, final List ploResults) { 116 | super(poQuery, pbSuccess, poErrorType, poThrowable); 117 | pullToRefresh = pbPullToRefresh; 118 | results = ploResults; 119 | } 120 | } 121 | //endregion 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/git_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoRoche/AndroidStarter/e41f0c2cf53646b48d95454044bfdea7d042a27d/app/src/main/res/drawable/git_icon.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_repo_detail.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_repo_list.xml: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/cell_repo.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 21 | 22 | 33 | 34 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_repo_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 21 | 22 | 28 | 29 | 34 | 35 | 42 | 43 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_repo_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 35 | 36 | 43 | 44 | 45 | 46 | 52 | 53 | 54 | 58 | 59 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoRoche/AndroidStarter/e41f0c2cf53646b48d95454044bfdea7d042a27d/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoRoche/AndroidStarter/e41f0c2cf53646b48d95454044bfdea7d042a27d/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoRoche/AndroidStarter/e41f0c2cf53646b48d95454044bfdea7d042a27d/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoRoche/AndroidStarter/e41f0c2cf53646b48d95454044bfdea7d042a27d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoRoche/AndroidStarter/e41f0c2cf53646b48d95454044bfdea7d042a27d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/raw/ormlite_config.txt: -------------------------------------------------------------------------------- 1 | # --table-start-- 2 | dataClass=fr.guddy.androidstarter.persistence.entities.AbstractOrmLiteEntity 3 | tableName=abstractormliteentity 4 | # --table-fields-start-- 5 | # --field-start-- 6 | fieldName=_id 7 | columnName=_id 8 | generatedId=true 9 | # --field-end-- 10 | # --table-fields-end-- 11 | # --table-end-- 12 | ################################# 13 | # --table-start-- 14 | dataClass=fr.guddy.androidstarter.persistence.entities.RepoEntity 15 | tableName=REPO 16 | # --table-fields-start-- 17 | # --field-start-- 18 | fieldName=id 19 | # --field-end-- 20 | # --field-start-- 21 | fieldName=name 22 | # --field-end-- 23 | # --field-start-- 24 | fieldName=description 25 | # --field-end-- 26 | # --field-start-- 27 | fieldName=url 28 | # --field-end-- 29 | # --field-start-- 30 | fieldName=avatarUrl 31 | # --field-end-- 32 | # --field-start-- 33 | fieldName=_id 34 | columnName=_id 35 | generatedId=true 36 | # --field-end-- 37 | # --table-fields-end-- 38 | # --table-end-- 39 | ################################# 40 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | > 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #81D4FA 4 | #0277BD 5 | #FDD835 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 200dp 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidStarter 3 | Repo Detail 4 | Aucun repo disponible 5 | Erreur pendant la récupération des repos 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |