├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── fabric.properties ├── keystore │ └── debug.keystore ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── uk │ │ └── co │ │ └── ribot │ │ └── androidboilerplate │ │ ├── MainActivityTest.java │ │ ├── runner │ │ ├── RxAndroidJUnitRunner.java │ │ └── UnlockDeviceAndroidJUnitRunner.java │ │ └── util │ │ └── RxEspressoScheduleHandler.java │ ├── commonTest │ └── java │ │ └── uk │ │ └── co │ │ └── ribot │ │ └── androidboilerplate │ │ └── test │ │ └── common │ │ ├── TestComponentRule.java │ │ ├── TestDataFactory.java │ │ └── injection │ │ ├── component │ │ └── TestComponent.java │ │ └── module │ │ └── ApplicationTestModule.java │ ├── debug │ └── AndroidManifest.xml │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── uk │ │ │ └── co │ │ │ └── ribot │ │ │ └── androidboilerplate │ │ │ ├── BoilerplateApplication.java │ │ │ ├── data │ │ │ ├── DataManager.java │ │ │ ├── SyncService.java │ │ │ ├── local │ │ │ │ ├── DatabaseHelper.java │ │ │ │ ├── Db.java │ │ │ │ ├── DbOpenHelper.java │ │ │ │ └── PreferencesHelper.java │ │ │ ├── model │ │ │ │ ├── Name.java │ │ │ │ ├── Profile.java │ │ │ │ └── Ribot.java │ │ │ └── remote │ │ │ │ └── RibotsService.java │ │ │ ├── injection │ │ │ ├── ActivityContext.java │ │ │ ├── ApplicationContext.java │ │ │ ├── ConfigPersistent.java │ │ │ ├── PerActivity.java │ │ │ ├── component │ │ │ │ ├── ActivityComponent.java │ │ │ │ ├── ApplicationComponent.java │ │ │ │ └── ConfigPersistentComponent.java │ │ │ └── module │ │ │ │ ├── ActivityModule.java │ │ │ │ └── ApplicationModule.java │ │ │ ├── ui │ │ │ ├── base │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── BasePresenter.java │ │ │ │ ├── MvpView.java │ │ │ │ └── Presenter.java │ │ │ └── main │ │ │ │ ├── MainActivity.java │ │ │ │ ├── MainMvpView.java │ │ │ │ ├── MainPresenter.java │ │ │ │ └── RibotsAdapter.java │ │ │ └── util │ │ │ ├── AndroidComponentUtil.java │ │ │ ├── DialogFactory.java │ │ │ ├── MyGsonTypeAdapterFactory.java │ │ │ ├── NetworkUtil.java │ │ │ ├── RxEventBus.java │ │ │ ├── RxUtil.java │ │ │ └── ViewUtil.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ └── item_ribot.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── uk │ └── co │ └── ribot │ └── androidboilerplate │ ├── DataManagerTest.java │ ├── DatabaseHelperTest.java │ ├── MainPresenterTest.java │ └── util │ ├── DefaultConfig.java │ ├── RxEventBusTest.java │ └── RxSchedulersOverrideRule.java ├── build.gradle ├── config └── quality │ ├── checkstyle │ └── checkstyle-config.xml │ ├── findbugs │ └── android-exclude-filter.xml │ ├── pmd │ └── pmd-ruleset.xml │ └── quality.gradle ├── crashlytics_release_notes.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── check-task-diagram.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | /build 6 | .idea/ 7 | *iml 8 | *.iml 9 | */build -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions to help keep the boilerplate up-to-date with the latest versions and best practices. 4 | 5 | Simply fork the repository to make your changes, then open a pull request with a little comment about what you are proposing to change and why. 6 | Be sure to stick to our [code style guidelines](https://github.com/ribot/android-guidelines) and make sure the tests pass. 7 | 8 | Please note: this is a boilerplate project from which we intend to make it as easy as possible to start new projects. As such, we'd like to keep the features to a minimum and steer away from merging "showcase features". -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 Ribot Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Thank you for your interest in ribot’s development work. Unfortunately there are no current plans to make any changes to this project in the near future, and it is not being maintained or updated. We hope you still find this a useful resource and you can check out the newest recommendations in [Android Jetpack](https://developer.android.com/jetpack/) for more ideas! 2 | 3 | # Android Boilerplate 4 | 5 | Sample Android app that we use at [ribot](http://ribot.co.uk) as a reference for new Android projects. It demonstrates the architecture, tools and guidelines that we use when developing for the Android platform (https://github.com/ribot/android-guidelines) 6 | 7 | Libraries and tools included: 8 | 9 | - Support libraries 10 | - RecyclerViews and CardViews 11 | - [RxJava](https://github.com/ReactiveX/RxJava) and [RxAndroid](https://github.com/ReactiveX/RxAndroid) 12 | - [Retrofit 2](http://square.github.io/retrofit/) 13 | - [Dagger 2](http://google.github.io/dagger/) 14 | - [SqlBrite](https://github.com/square/sqlbrite) 15 | - [Butterknife](https://github.com/JakeWharton/butterknife) 16 | - [Timber](https://github.com/JakeWharton/timber) 17 | - [Glide](https://github.com/bumptech/glide) 18 | - [AutoValue](https://github.com/google/auto/tree/master/value) with extensions [AutoValueParcel](https://github.com/rharter/auto-value-parcel) and [AutoValueGson](https://github.com/rharter/auto-value-gson) 19 | - Functional tests with [Espresso](https://google.github.io/android-testing-support-library/docs/espresso/index.html) 20 | - [Robolectric](http://robolectric.org/) 21 | - [Mockito](http://mockito.org/) 22 | - [Checkstyle](http://checkstyle.sourceforge.net/), [PMD](https://pmd.github.io/) and [Findbugs](http://findbugs.sourceforge.net/) for code analysis 23 | 24 | ## Requirements 25 | 26 | - JDK 1.8 27 | - [Android SDK](http://developer.android.com/sdk/index.html). 28 | - Android N [(API 24) ](http://developer.android.com/tools/revisions/platforms.html). 29 | - Latest Android SDK Tools and build tools. 30 | 31 | 32 | ## Architecture 33 | 34 | This project follows ribot's Android architecture guidelines that are based on [MVP (Model View Presenter)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter). Read more about them [here](https://github.com/ribot/android-guidelines/blob/master/architecture_guidelines/android_architecture.md). 35 | 36 | ![](https://github.com/ribot/android-guidelines/raw/master/architecture_guidelines/architecture_diagram.png) 37 | 38 | ### How to implement a new screen following MVP 39 | 40 | Imagine you have to implement a sign in screen. 41 | 42 | 1. Create a new package under `ui` called `signin` 43 | 2. Create an new Activity called `ActivitySignIn`. You could also use a Fragment. 44 | 3. Define the view interface that your Activity is going to implement. Create a new interface called `SignInMvpView` that extends `MvpView`. Add the methods that you think will be necessary, e.g. `showSignInSuccessful()` 45 | 4. Create a `SignInPresenter` class that extends `BasePresenter` 46 | 5. Implement the methods in `SignInPresenter` that your Activity requires to perform the necessary actions, e.g. `signIn(String email)`. Once the sign in action finishes you should call `getMvpView().showSignInSuccessful()`. 47 | 6. Create a `SignInPresenterTest`and write unit tests for `signIn(email)`. Remember to mock the `SignInMvpView` and also the `DataManager`. 48 | 7. Make your `ActivitySignIn` implement `SignInMvpView` and implement the required methods like `showSignInSuccessful()` 49 | 8. In your activity, inject a new instance of `SignInPresenter` and call `presenter.attachView(this)` from `onCreate` and `presenter.detachView()` from `onDestroy()`. Also, set up a click listener in your button that calls `presenter.signIn(email)`. 50 | 51 | ## Code Quality 52 | 53 | This project integrates a combination of unit tests, functional test and code analysis tools. 54 | 55 | ### Tests 56 | 57 | To run **unit** tests on your machine: 58 | 59 | ``` 60 | ./gradlew test 61 | ``` 62 | 63 | To run **functional** tests on connected devices: 64 | 65 | ``` 66 | ./gradlew connectedAndroidTest 67 | ``` 68 | 69 | Note: For Android Studio to use syntax highlighting for Automated tests and Unit tests you **must** switch the Build Variant to the desired mode. 70 | 71 | ### Code Analysis tools 72 | 73 | The following code analysis tools are set up on this project: 74 | 75 | * [PMD](https://pmd.github.io/): It finds common programming flaws like unused variables, empty catch blocks, unnecessary object creation, and so forth. See [this project's PMD ruleset](config/quality/pmd/pmd-ruleset.xml). 76 | 77 | ``` 78 | ./gradlew pmd 79 | ``` 80 | 81 | * [Findbugs](http://findbugs.sourceforge.net/): This tool uses static analysis to find bugs in Java code. Unlike PMD, it uses compiled Java bytecode instead of source code. 82 | 83 | ``` 84 | ./gradlew findbugs 85 | ``` 86 | 87 | * [Checkstyle](http://checkstyle.sourceforge.net/): It ensures that the code style follows [our Android code guidelines](https://github.com/ribot/android-guidelines/blob/master/project_and_code_guidelines.md#2-code-guidelines). See our [checkstyle config file](config/quality/checkstyle/checkstyle-config.xml). 88 | 89 | ``` 90 | ./gradlew checkstyle 91 | ``` 92 | 93 | ### The check task 94 | 95 | To ensure that your code is valid and stable use check: 96 | 97 | ``` 98 | ./gradlew check 99 | ``` 100 | 101 | This will run all the code analysis tools and unit tests in the following order: 102 | 103 | ![Check Diagram](images/check-task-diagram.png) 104 | 105 | ## Distribution 106 | 107 | The project can be distributed using either [Crashlytics](http://support.crashlytics.com/knowledgebase/articles/388925-beta-distributions-with-gradle) or the [Google Play Store](https://github.com/Triple-T/gradle-play-publisher). 108 | 109 | ### Play Store 110 | 111 | We use the __Gradle Play Publisher__ plugin. Once set up correctly, you will be able to push new builds to 112 | the Alpha, Beta or production channels like this 113 | 114 | ``` 115 | ./gradlew publishApkRelease 116 | ``` 117 | Read [plugin documentation](https://github.com/Triple-T/gradle-play-publisher) for more info. 118 | 119 | ### Crashlytics 120 | 121 | You can also use Fabric's Crashlytics for distributing beta releases. Remember to add your fabric 122 | account details to `app/src/fabric.properties`. 123 | 124 | To upload a release build to Crashlytics run: 125 | 126 | ``` 127 | ./gradlew assembleRelease crashlyticsUploadDistributionRelease 128 | ``` 129 | 130 | ## New project setup 131 | 132 | To quickly start a new project from this boilerplate follow the next steps: 133 | 134 | * Download this [repository as a zip](https://github.com/ribot/android-boilerplate/archive/master.zip). 135 | * Change the package name. 136 | * Rename packages in main, androidTest and test using Android Studio. 137 | * In `app/build.gradle` file, `packageName` and `testInstrumentationRunner`. 138 | * In `src/main/AndroidManifest.xml` and `src/debug/AndroidManifest.xml`. 139 | * Create a new git repository, [see GitHub tutorial](https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/). 140 | * Replace the example code with your app code following the same architecture. 141 | * In `app/build.gradle` add the signing config to enable release versions. 142 | * Add Fabric API key and secret to fabric.properties and uncomment Fabric plugin set up in `app/build.gradle` 143 | * Update `proguard-rules.pro` to keep models (see TODO in file) and add extra rules to file if needed. 144 | * Update README with information relevant to the new project. 145 | * Update LICENSE to match the requirements of the new project. 146 | 147 | ## License 148 | 149 | ``` 150 | Copyright 2015 Ribot Ltd. 151 | 152 | Licensed under the Apache License, Version 2.0 (the "License"); 153 | you may not use this file except in compliance with the License. 154 | You may obtain a copy of the License at 155 | 156 | http://www.apache.org/licenses/LICENSE-2.0 157 | 158 | Unless required by applicable law or agreed to in writing, software 159 | distributed under the License is distributed on an "AS IS" BASIS, 160 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 161 | See the License for the specific language governing permissions and 162 | limitations under the License. 163 | ``` 164 | 165 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml 4 | .idea -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply from: '../config/quality/quality.gradle' 3 | apply plugin: 'com.github.triplet.play' 4 | //TODO uncomment line below after adding fabric api secret and key to fabric.properties 5 | //apply plugin: 'io.fabric' 6 | 7 | android { 8 | compileSdkVersion 25 9 | buildToolsVersion '25.0.3' 10 | 11 | defaultConfig { 12 | applicationId 'uk.co.ribot.androidboilerplate' 13 | minSdkVersion 16 14 | targetSdkVersion 25 15 | testInstrumentationRunner "${applicationId}.runner.RxAndroidJUnitRunner" 16 | versionCode 1000 17 | // Major -> Millions, Minor -> Thousands, Bugfix -> Hundreds. E.g 1.3.72 == 1,003,072 18 | versionName '0.1.0' 19 | } 20 | 21 | signingConfigs { 22 | // You must set up an environment var before release signing 23 | // Run: export APP_KEY={password} 24 | // TODO Add your release keystore in /keystore folder 25 | release { 26 | storeFile file('keystore/release.keystore') 27 | keyAlias 'alias' 28 | storePassword "$System.env.APP_KEY" 29 | keyPassword "$System.env.APP_KEY" 30 | } 31 | 32 | debug { 33 | storeFile file('keystore/debug.keystore') 34 | keyAlias 'androiddebugkey' 35 | storePassword 'android' 36 | keyPassword 'android' 37 | } 38 | } 39 | 40 | buildTypes { 41 | release { 42 | signingConfig signingConfigs.release 43 | 44 | minifyEnabled true 45 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 46 | 47 | ext.betaDistributionReleaseNotesFilePath = 48 | file('../crashlytics_release_notes.txt').absolutePath 49 | } 50 | 51 | debug { 52 | versionNameSuffix " Debug" 53 | debuggable true 54 | } 55 | } 56 | 57 | sourceSets { 58 | def commonTestDir = 'src/commonTest/java' 59 | test { 60 | java.srcDir commonTestDir 61 | } 62 | androidTest { 63 | java.srcDir commonTestDir 64 | } 65 | } 66 | 67 | //Needed because of this https://github.com/square/okio/issues/58 68 | lintOptions { 69 | warning 'InvalidPackage' 70 | } 71 | } 72 | 73 | play { 74 | serviceAccountEmail = 'your-service-account-email' 75 | pk12File = file('key.p12') 76 | // By default publishes to Alpha channel 77 | track = 'alpha' 78 | } 79 | 80 | dependencies { 81 | final PLAY_SERVICES_VERSION = '9.6.1' 82 | final SUPPORT_LIBRARY_VERSION = '25.3.1' 83 | final RETROFIT_VERSION = '2.3.0' 84 | final DAGGER_VERSION = '2.5' 85 | final MOCKITO_VERSION = '2.6.2' 86 | final HAMCREST_VERSION = '1.3' 87 | final ESPRESSO_VERSION = '2.2.1' 88 | final RUNNER_VERSION = '0.4' 89 | final BUTTERKNIFE_VERSION = '8.4.0' 90 | final AUTO_VALUE_VERSION = '1.3' 91 | final AUTO_VALUE_GSON_VERSION = '0.4.2' 92 | 93 | def daggerCompiler = "com.google.dagger:dagger-compiler:$DAGGER_VERSION" 94 | def jUnit = "junit:junit:4.12" 95 | def mockito = "org.mockito:mockito-core:$MOCKITO_VERSION" 96 | 97 | // App Dependencies 98 | compile "com.google.android.gms:play-services-gcm:$PLAY_SERVICES_VERSION" 99 | compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION" 100 | compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION" 101 | compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION" 102 | compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" 103 | 104 | compile ("com.squareup.sqlbrite2:sqlbrite:2.0.0") { 105 | exclude group: 'com.android.support', module: 'support-annotations' 106 | } 107 | compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION" 108 | compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION" 109 | compile "com.squareup.retrofit2:adapter-rxjava2:$RETROFIT_VERSION" 110 | 111 | compile 'com.github.bumptech.glide:glide:3.7.0' 112 | compile 'io.reactivex.rxjava2:rxandroid:2.0.1' 113 | compile 'io.reactivex.rxjava2:rxjava:2.1.0' 114 | compile('com.crashlytics.sdk.android:crashlytics:2.5.7@aar') { 115 | transitive = true; 116 | } 117 | 118 | compile 'com.jakewharton.timber:timber:4.1.2' 119 | compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" 120 | annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" 121 | 122 | // Replace provided dependency below with official AutoValue once this issue is fixed 123 | // https://github.com/google/auto/issues/268 124 | provided "com.jakewharton.auto.value:auto-value-annotations:$AUTO_VALUE_VERSION" 125 | provided "com.ryanharter.auto.value:auto-value-gson:$AUTO_VALUE_GSON_VERSION" 126 | annotationProcessor "com.google.auto.value:auto-value:$AUTO_VALUE_VERSION" 127 | annotationProcessor 'com.ryanharter.auto.value:auto-value-parcel:0.2.4-rc2' 128 | annotationProcessor "com.ryanharter.auto.value:auto-value-gson:$AUTO_VALUE_GSON_VERSION" 129 | annotationProcessor 'com.squareup:javapoet:1.7.0' 130 | // https://github.com/rharter/auto-value-parcel/issues/64 131 | 132 | compile "com.google.dagger:dagger:$DAGGER_VERSION" 133 | provided 'org.glassfish:javax.annotation:10.0-b28' //Required by Dagger2 134 | annotationProcessor daggerCompiler 135 | testAnnotationProcessor daggerCompiler 136 | androidTestAnnotationProcessor daggerCompiler 137 | 138 | // Instrumentation test dependencies 139 | androidTestCompile jUnit 140 | androidTestCompile mockito 141 | androidTestCompile "org.mockito:mockito-android:$MOCKITO_VERSION" 142 | androidTestCompile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" 143 | androidTestCompile("com.android.support.test.espresso:espresso-contrib:$ESPRESSO_VERSION") { 144 | exclude group: 'com.android.support', module: 'appcompat' 145 | exclude group: 'com.android.support', module: 'support-v4' 146 | exclude group: 'com.android.support', module: 'recyclerview-v7' 147 | } 148 | androidTestCompile "com.android.support.test.espresso:espresso-core:$ESPRESSO_VERSION" 149 | androidTestCompile "com.android.support.test:runner:$RUNNER_VERSION" 150 | androidTestCompile "com.android.support.test:rules:$RUNNER_VERSION" 151 | 152 | // Unit tests dependencies 153 | testCompile jUnit 154 | testCompile mockito 155 | testCompile "org.hamcrest:hamcrest-core:$HAMCREST_VERSION" 156 | testCompile "org.hamcrest:hamcrest-library:$HAMCREST_VERSION" 157 | testCompile "org.hamcrest:hamcrest-integration:$HAMCREST_VERSION" 158 | testCompile 'org.robolectric:robolectric:3.3' 159 | } 160 | 161 | // Log out test results to console 162 | tasks.matching {it instanceof Test}.all { 163 | testLogging.events = ["failed", "passed", "skipped"] 164 | } 165 | -------------------------------------------------------------------------------- /app/fabric.properties: -------------------------------------------------------------------------------- 1 | apiSecret=changeMeToYourRealApiSecret 2 | apiKey=changeMeToYourRealApiKey -------------------------------------------------------------------------------- /app/keystore/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ribot/android-boilerplate/74f576f9da0c7cc16ccd88221d443aa7d02a9ded/app/keystore/debug.keystore -------------------------------------------------------------------------------- /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 /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 | # ButterKnife rules 20 | -keep class butterknife.** { *; } 21 | -dontwarn butterknife.internal.** 22 | -keep class **$$ViewBinder { *; } 23 | 24 | -keepclasseswithmembernames class * { 25 | @butterknife.* ; 26 | } 27 | 28 | -keepclasseswithmembernames class * { 29 | @butterknife.* ; 30 | } 31 | 32 | # Retrofit rules 33 | # Platform calls Class.forName on types which do not exist on Android to determine platform. 34 | -dontnote retrofit2.Platform 35 | # Platform used when running on RoboVM on iOS. Will not be used at runtime. 36 | -dontnote retrofit2.Platform$IOS$MainThreadExecutor 37 | # Platform used when running on Java 8 VMs. Will not be used at runtime. 38 | -dontwarn retrofit2.Platform$Java8 39 | # Retain generic type information for use by reflection by converters and adapters. 40 | -keepattributes Signature 41 | # Retain declared checked exceptions for use by a Proxy instance. 42 | -keepattributes Exceptions 43 | 44 | # OkHttp rules 45 | -dontwarn okio.** 46 | -dontwarn com.squareup.okhttp.** 47 | 48 | # Otto rules 49 | -keepclassmembers class ** { 50 | @com.squareup.otto.Subscribe public *; 51 | @com.squareup.otto.Produce public *; 52 | } 53 | 54 | # RxJava rules 55 | # RxAndroid will soon ship with rules so this may not be needed in the future 56 | # https://github.com/ReactiveX/RxAndroid/issues/219 57 | -dontwarn sun.misc.Unsafe 58 | -keep class rx.internal.util.unsafe.** { *; } 59 | 60 | # Gson rules 61 | -keepattributes Signature 62 | -keep class sun.misc.Unsafe { *; } 63 | # TODO change to match your package model 64 | # Keep non static or private fields of models so Gson can find their names 65 | -keepclassmembers class uk.co.ribot.androidboilerplate.data.model.** { 66 | !static !private ; 67 | } 68 | # TODO change to match your Retrofit services (only if using inner models withing the service) 69 | # Some models used by gson are inner classes inside the retrofit service 70 | -keepclassmembers class uk.co.ribot.androidboilerplate.data.remote.RibotsService$** { 71 | !static !private ; 72 | } 73 | 74 | # Produces useful obfuscated stack traces 75 | # http://proguard.sourceforge.net/manual/examples.html#stacktrace 76 | -renamesourcefileattribute SourceFile 77 | -keepattributes SourceFile,LineNumberTable 78 | -------------------------------------------------------------------------------- /app/src/androidTest/java/uk/co/ribot/androidboilerplate/MainActivityTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate; 2 | 3 | import android.content.Intent; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.espresso.contrib.RecyclerViewActions; 6 | import android.support.test.rule.ActivityTestRule; 7 | import android.support.test.runner.AndroidJUnit4; 8 | 9 | import org.junit.Rule; 10 | import org.junit.Test; 11 | import org.junit.rules.RuleChain; 12 | import org.junit.rules.TestRule; 13 | import org.junit.runner.RunWith; 14 | 15 | import java.util.List; 16 | 17 | import io.reactivex.Observable; 18 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 19 | import uk.co.ribot.androidboilerplate.test.common.TestComponentRule; 20 | import uk.co.ribot.androidboilerplate.test.common.TestDataFactory; 21 | import uk.co.ribot.androidboilerplate.ui.main.MainActivity; 22 | 23 | import static android.support.test.espresso.Espresso.onView; 24 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 25 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 26 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 27 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 28 | import static org.mockito.Mockito.when; 29 | 30 | @RunWith(AndroidJUnit4.class) 31 | public class MainActivityTest { 32 | 33 | public final TestComponentRule component = 34 | new TestComponentRule(InstrumentationRegistry.getTargetContext()); 35 | public final ActivityTestRule main = 36 | new ActivityTestRule(MainActivity.class, false, false) { 37 | @Override 38 | protected Intent getActivityIntent() { 39 | // Override the default intent so we pass a false flag for syncing so it doesn't 40 | // start a sync service in the background that would affect the behaviour of 41 | // this test. 42 | return MainActivity.getStartIntent( 43 | InstrumentationRegistry.getTargetContext(), false); 44 | } 45 | }; 46 | 47 | // TestComponentRule needs to go first to make sure the Dagger ApplicationTestComponent is set 48 | // in the Application before any Activity is launched. 49 | @Rule 50 | public final TestRule chain = RuleChain.outerRule(component).around(main); 51 | 52 | @Test 53 | public void listOfRibotsShows() { 54 | List testDataRibots = TestDataFactory.makeListRibots(20); 55 | when(component.getMockDataManager().getRibots()) 56 | .thenReturn(Observable.just(testDataRibots)); 57 | 58 | main.launchActivity(null); 59 | 60 | int position = 0; 61 | for (Ribot ribot : testDataRibots) { 62 | onView(withId(R.id.recycler_view)) 63 | .perform(RecyclerViewActions.scrollToPosition(position)); 64 | String name = String.format("%s %s", ribot.profile().name().first(), 65 | ribot.profile().name().last()); 66 | onView(withText(name)) 67 | .check(matches(isDisplayed())); 68 | onView(withText(ribot.profile().email())) 69 | .check(matches(isDisplayed())); 70 | position++; 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/src/androidTest/java/uk/co/ribot/androidboilerplate/runner/RxAndroidJUnitRunner.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.runner; 2 | 3 | import android.os.Bundle; 4 | import android.support.test.espresso.Espresso; 5 | 6 | import io.reactivex.plugins.RxJavaPlugins; 7 | import uk.co.ribot.androidboilerplate.util.RxEspressoScheduleHandler; 8 | 9 | /** 10 | * Runner that registers a Espresso Indling resource that handles waiting for 11 | * RxJava Observables to finish. 12 | * WARNING - Using this runner will block the tests if the application uses long-lived hot 13 | * Observables such us event buses, etc. 14 | */ 15 | public class RxAndroidJUnitRunner extends UnlockDeviceAndroidJUnitRunner { 16 | 17 | @Override 18 | public void onCreate(Bundle arguments) { 19 | super.onCreate(arguments); 20 | 21 | RxEspressoScheduleHandler rxEspressoScheduleHandler = new RxEspressoScheduleHandler(); 22 | RxJavaPlugins.setScheduleHandler(rxEspressoScheduleHandler); 23 | Espresso.registerIdlingResources(rxEspressoScheduleHandler.getIdlingResource()); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/androidTest/java/uk/co/ribot/androidboilerplate/runner/UnlockDeviceAndroidJUnitRunner.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.runner; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Application; 5 | import android.app.KeyguardManager; 6 | import android.os.PowerManager; 7 | import android.support.test.runner.AndroidJUnitRunner; 8 | 9 | import static android.content.Context.KEYGUARD_SERVICE; 10 | import static android.content.Context.POWER_SERVICE; 11 | import static android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP; 12 | import static android.os.PowerManager.FULL_WAKE_LOCK; 13 | import static android.os.PowerManager.ON_AFTER_RELEASE; 14 | 15 | /** 16 | * Extension of AndroidJUnitRunner that adds some functionality to unblock the device screen 17 | * before starting the tests. 18 | */ 19 | public class UnlockDeviceAndroidJUnitRunner extends AndroidJUnitRunner { 20 | 21 | private PowerManager.WakeLock mWakeLock; 22 | 23 | @SuppressLint("MissingPermission") 24 | @Override 25 | public void onStart() { 26 | Application application = (Application) getTargetContext().getApplicationContext(); 27 | String simpleName = UnlockDeviceAndroidJUnitRunner.class.getSimpleName(); 28 | // Unlock the device so that the tests can input keystrokes. 29 | ((KeyguardManager) application.getSystemService(KEYGUARD_SERVICE)) 30 | .newKeyguardLock(simpleName) 31 | .disableKeyguard(); 32 | // Wake up the screen. 33 | PowerManager powerManager = ((PowerManager) application.getSystemService(POWER_SERVICE)); 34 | mWakeLock = powerManager.newWakeLock(FULL_WAKE_LOCK | ACQUIRE_CAUSES_WAKEUP | 35 | ON_AFTER_RELEASE, simpleName); 36 | mWakeLock.acquire(); 37 | super.onStart(); 38 | } 39 | 40 | @Override 41 | public void onDestroy() { 42 | super.onDestroy(); 43 | mWakeLock.release(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/androidTest/java/uk/co/ribot/androidboilerplate/util/RxEspressoScheduleHandler.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | import android.support.test.espresso.IdlingResource; 4 | import android.support.test.espresso.contrib.CountingIdlingResource; 5 | 6 | import io.reactivex.annotations.NonNull; 7 | import io.reactivex.functions.Function; 8 | 9 | /** 10 | * Espresso Idling resource that handles waiting for RxJava Observables executions. 11 | * This class must be used with RxIdlingExecutionHook. 12 | * Before registering this idling resource you must: 13 | * 1. Create an instance of this class. 14 | * 2. Register RxEspressoScheduleHandler with the RxJavaPlugins using setScheduleHandler() 15 | * 3. Register this idle resource with Espresso using Espresso.registerIdlingResources() 16 | */ 17 | public class RxEspressoScheduleHandler implements Function { 18 | 19 | private final CountingIdlingResource mCountingIdlingResource = 20 | new CountingIdlingResource("rxJava"); 21 | 22 | @Override 23 | public Runnable apply(@NonNull final Runnable runnable) throws Exception { 24 | return new Runnable() { 25 | @Override 26 | public void run() { 27 | mCountingIdlingResource.increment(); 28 | 29 | try { 30 | runnable.run(); 31 | } finally { 32 | mCountingIdlingResource.decrement(); 33 | } 34 | } 35 | }; 36 | } 37 | 38 | public IdlingResource getIdlingResource() { 39 | return mCountingIdlingResource; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/commonTest/java/uk/co/ribot/androidboilerplate/test/common/TestComponentRule.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.test.common; 2 | 3 | import android.content.Context; 4 | 5 | import org.junit.rules.TestRule; 6 | import org.junit.runner.Description; 7 | import org.junit.runners.model.Statement; 8 | 9 | import uk.co.ribot.androidboilerplate.BoilerplateApplication; 10 | import uk.co.ribot.androidboilerplate.data.DataManager; 11 | import uk.co.ribot.androidboilerplate.test.common.injection.component.DaggerTestComponent; 12 | import uk.co.ribot.androidboilerplate.test.common.injection.component.TestComponent; 13 | import uk.co.ribot.androidboilerplate.test.common.injection.module.ApplicationTestModule; 14 | 15 | /** 16 | * Test rule that creates and sets a Dagger TestComponent into the application overriding the 17 | * existing application component. 18 | * Use this rule in your test case in order for the app to use mock dependencies. 19 | * It also exposes some of the dependencies so they can be easily accessed from the tests, e.g. to 20 | * stub mocks etc. 21 | */ 22 | public class TestComponentRule implements TestRule { 23 | 24 | private final TestComponent mTestComponent; 25 | private final Context mContext; 26 | 27 | public TestComponentRule(Context context) { 28 | mContext = context; 29 | BoilerplateApplication application = BoilerplateApplication.get(context); 30 | mTestComponent = DaggerTestComponent.builder() 31 | .applicationTestModule(new ApplicationTestModule(application)) 32 | .build(); 33 | } 34 | 35 | public Context getContext() { 36 | return mContext; 37 | } 38 | 39 | public DataManager getMockDataManager() { 40 | return mTestComponent.dataManager(); 41 | } 42 | 43 | @Override 44 | public Statement apply(final Statement base, Description description) { 45 | return new Statement() { 46 | @Override 47 | public void evaluate() throws Throwable { 48 | BoilerplateApplication application = BoilerplateApplication.get(mContext); 49 | application.setComponent(mTestComponent); 50 | base.evaluate(); 51 | application.setComponent(null); 52 | } 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/commonTest/java/uk/co/ribot/androidboilerplate/test/common/TestDataFactory.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.test.common; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Date; 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | import uk.co.ribot.androidboilerplate.data.model.Name; 9 | import uk.co.ribot.androidboilerplate.data.model.Profile; 10 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 11 | 12 | /** 13 | * Factory class that makes instances of data models with random field values. 14 | * The aim of this class is to help setting up test fixtures. 15 | */ 16 | public class TestDataFactory { 17 | 18 | public static String randomUuid() { 19 | return UUID.randomUUID().toString(); 20 | } 21 | 22 | public static Ribot makeRibot(String uniqueSuffix) { 23 | return Ribot.create(makeProfile(uniqueSuffix)); 24 | } 25 | 26 | public static List makeListRibots(int number) { 27 | List ribots = new ArrayList<>(); 28 | for (int i = 0; i < number; i++) { 29 | ribots.add(makeRibot(String.valueOf(i))); 30 | } 31 | return ribots; 32 | } 33 | 34 | public static Profile makeProfile(String uniqueSuffix) { 35 | return Profile.builder() 36 | .setName(makeName(uniqueSuffix)) 37 | .setEmail("email" + uniqueSuffix + "@ribot.co.uk") 38 | .setDateOfBirth(new Date()) 39 | .setHexColor("#0066FF") 40 | .setAvatar("http://api.ribot.io/images/" + uniqueSuffix) 41 | .setBio(randomUuid()) 42 | .build(); 43 | } 44 | 45 | public static Name makeName(String uniqueSuffix) { 46 | return Name.create("Name-" + uniqueSuffix, "Surname-" + uniqueSuffix); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /app/src/commonTest/java/uk/co/ribot/androidboilerplate/test/common/injection/component/TestComponent.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.test.common.injection.component; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import dagger.Component; 6 | import uk.co.ribot.androidboilerplate.injection.component.ApplicationComponent; 7 | import uk.co.ribot.androidboilerplate.test.common.injection.module.ApplicationTestModule; 8 | 9 | @Singleton 10 | @Component(modules = ApplicationTestModule.class) 11 | public interface TestComponent extends ApplicationComponent { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/commonTest/java/uk/co/ribot/androidboilerplate/test/common/injection/module/ApplicationTestModule.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.test.common.injection.module; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | import uk.co.ribot.androidboilerplate.data.DataManager; 11 | import uk.co.ribot.androidboilerplate.data.remote.RibotsService; 12 | import uk.co.ribot.androidboilerplate.injection.ApplicationContext; 13 | 14 | import static org.mockito.Mockito.mock; 15 | 16 | /** 17 | * Provides application-level dependencies for an app running on a testing environment 18 | * This allows injecting mocks if necessary. 19 | */ 20 | @Module 21 | public class ApplicationTestModule { 22 | 23 | private final Application mApplication; 24 | 25 | public ApplicationTestModule(Application application) { 26 | mApplication = application; 27 | } 28 | 29 | @Provides 30 | Application provideApplication() { 31 | return mApplication; 32 | } 33 | 34 | @Provides 35 | @ApplicationContext 36 | Context provideContext() { 37 | return mApplication; 38 | } 39 | 40 | /************* MOCKS *************/ 41 | 42 | @Provides 43 | @Singleton 44 | DataManager provideDataManager() { 45 | return mock(DataManager.class); 46 | } 47 | 48 | @Provides 49 | @Singleton 50 | RibotsService provideRibotsService() { 51 | return mock(RibotsService.class); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/BoilerplateApplication.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.crashlytics.android.Crashlytics; 7 | 8 | import io.fabric.sdk.android.Fabric; 9 | import timber.log.Timber; 10 | import uk.co.ribot.androidboilerplate.injection.component.ApplicationComponent; 11 | import uk.co.ribot.androidboilerplate.injection.component.DaggerApplicationComponent; 12 | import uk.co.ribot.androidboilerplate.injection.module.ApplicationModule; 13 | 14 | public class BoilerplateApplication extends Application { 15 | 16 | ApplicationComponent mApplicationComponent; 17 | 18 | @Override 19 | public void onCreate() { 20 | super.onCreate(); 21 | 22 | if (BuildConfig.DEBUG) { 23 | Timber.plant(new Timber.DebugTree()); 24 | Fabric.with(this, new Crashlytics()); 25 | } 26 | } 27 | 28 | public static BoilerplateApplication get(Context context) { 29 | return (BoilerplateApplication) context.getApplicationContext(); 30 | } 31 | 32 | public ApplicationComponent getComponent() { 33 | if (mApplicationComponent == null) { 34 | mApplicationComponent = DaggerApplicationComponent.builder() 35 | .applicationModule(new ApplicationModule(this)) 36 | .build(); 37 | } 38 | return mApplicationComponent; 39 | } 40 | 41 | // Needed to replace the component with a test specific one 42 | public void setComponent(ApplicationComponent applicationComponent) { 43 | mApplicationComponent = applicationComponent; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/data/DataManager.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.data; 2 | 3 | import java.util.List; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Singleton; 7 | 8 | import io.reactivex.Observable; 9 | import io.reactivex.ObservableSource; 10 | import io.reactivex.annotations.NonNull; 11 | import io.reactivex.functions.Function; 12 | import uk.co.ribot.androidboilerplate.data.local.DatabaseHelper; 13 | import uk.co.ribot.androidboilerplate.data.local.PreferencesHelper; 14 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 15 | import uk.co.ribot.androidboilerplate.data.remote.RibotsService; 16 | 17 | @Singleton 18 | public class DataManager { 19 | 20 | private final RibotsService mRibotsService; 21 | private final DatabaseHelper mDatabaseHelper; 22 | private final PreferencesHelper mPreferencesHelper; 23 | 24 | @Inject 25 | public DataManager(RibotsService ribotsService, PreferencesHelper preferencesHelper, 26 | DatabaseHelper databaseHelper) { 27 | mRibotsService = ribotsService; 28 | mPreferencesHelper = preferencesHelper; 29 | mDatabaseHelper = databaseHelper; 30 | } 31 | 32 | public PreferencesHelper getPreferencesHelper() { 33 | return mPreferencesHelper; 34 | } 35 | 36 | public Observable syncRibots() { 37 | return mRibotsService.getRibots() 38 | .concatMap(new Function, ObservableSource>() { 39 | @Override 40 | public ObservableSource apply(@NonNull List ribots) 41 | throws Exception { 42 | return mDatabaseHelper.setRibots(ribots); 43 | } 44 | }); 45 | } 46 | 47 | public Observable> getRibots() { 48 | return mDatabaseHelper.getRibots().distinct(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/data/SyncService.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.data; 2 | 3 | import android.app.Service; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.net.ConnectivityManager; 8 | import android.os.IBinder; 9 | 10 | import javax.inject.Inject; 11 | 12 | import io.reactivex.Observer; 13 | import io.reactivex.annotations.NonNull; 14 | import io.reactivex.disposables.Disposable; 15 | import io.reactivex.schedulers.Schedulers; 16 | import timber.log.Timber; 17 | import uk.co.ribot.androidboilerplate.BoilerplateApplication; 18 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 19 | import uk.co.ribot.androidboilerplate.util.AndroidComponentUtil; 20 | import uk.co.ribot.androidboilerplate.util.NetworkUtil; 21 | import uk.co.ribot.androidboilerplate.util.RxUtil; 22 | 23 | public class SyncService extends Service { 24 | 25 | @Inject DataManager mDataManager; 26 | private Disposable mDisposable; 27 | 28 | public static Intent getStartIntent(Context context) { 29 | return new Intent(context, SyncService.class); 30 | } 31 | 32 | public static boolean isRunning(Context context) { 33 | return AndroidComponentUtil.isServiceRunning(context, SyncService.class); 34 | } 35 | 36 | @Override 37 | public void onCreate() { 38 | super.onCreate(); 39 | BoilerplateApplication.get(this).getComponent().inject(this); 40 | } 41 | 42 | @Override 43 | public int onStartCommand(Intent intent, int flags, final int startId) { 44 | Timber.i("Starting sync..."); 45 | 46 | if (!NetworkUtil.isNetworkConnected(this)) { 47 | Timber.i("Sync canceled, connection not available"); 48 | AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable.class, true); 49 | stopSelf(startId); 50 | return START_NOT_STICKY; 51 | } 52 | 53 | RxUtil.dispose(mDisposable); 54 | mDataManager.syncRibots() 55 | .subscribeOn(Schedulers.io()) 56 | .subscribe(new Observer() { 57 | @Override 58 | public void onSubscribe(@NonNull Disposable d) { 59 | mDisposable = d; 60 | } 61 | 62 | @Override 63 | public void onNext(@NonNull Ribot ribot) { 64 | } 65 | 66 | @Override 67 | public void onError(@NonNull Throwable e) { 68 | Timber.w(e, "Error syncing."); 69 | stopSelf(startId); 70 | } 71 | 72 | @Override 73 | public void onComplete() { 74 | Timber.i("Synced successfully!"); 75 | stopSelf(startId); 76 | } 77 | }); 78 | 79 | return START_STICKY; 80 | } 81 | 82 | @Override 83 | public void onDestroy() { 84 | if (mDisposable != null) mDisposable.dispose(); 85 | super.onDestroy(); 86 | } 87 | 88 | @Override 89 | public IBinder onBind(Intent intent) { 90 | return null; 91 | } 92 | 93 | public static class SyncOnConnectionAvailable extends BroadcastReceiver { 94 | 95 | @Override 96 | public void onReceive(Context context, Intent intent) { 97 | if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION) 98 | && NetworkUtil.isNetworkConnected(context)) { 99 | Timber.i("Connection is now available, triggering sync..."); 100 | AndroidComponentUtil.toggleComponent(context, this.getClass(), false); 101 | context.startService(getStartIntent(context)); 102 | } 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/data/local/DatabaseHelper.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.data.local; 2 | 3 | import android.database.Cursor; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.support.annotation.VisibleForTesting; 6 | 7 | import com.squareup.sqlbrite2.BriteDatabase; 8 | import com.squareup.sqlbrite2.SqlBrite; 9 | 10 | import java.util.Collection; 11 | import java.util.List; 12 | 13 | import javax.inject.Inject; 14 | import javax.inject.Singleton; 15 | 16 | import io.reactivex.Observable; 17 | import io.reactivex.ObservableEmitter; 18 | import io.reactivex.ObservableOnSubscribe; 19 | import io.reactivex.Scheduler; 20 | import io.reactivex.annotations.NonNull; 21 | import io.reactivex.functions.Function; 22 | import io.reactivex.schedulers.Schedulers; 23 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 24 | 25 | @Singleton 26 | public class DatabaseHelper { 27 | 28 | private final BriteDatabase mDb; 29 | 30 | @Inject 31 | public DatabaseHelper(DbOpenHelper dbOpenHelper) { 32 | this(dbOpenHelper, Schedulers.io()); 33 | } 34 | 35 | @VisibleForTesting 36 | public DatabaseHelper(DbOpenHelper dbOpenHelper, Scheduler scheduler) { 37 | SqlBrite.Builder briteBuilder = new SqlBrite.Builder(); 38 | mDb = briteBuilder.build().wrapDatabaseHelper(dbOpenHelper, scheduler); 39 | } 40 | 41 | public BriteDatabase getBriteDb() { 42 | return mDb; 43 | } 44 | 45 | public Observable setRibots(final Collection newRibots) { 46 | return Observable.create(new ObservableOnSubscribe() { 47 | @Override 48 | public void subscribe(ObservableEmitter emitter) throws Exception { 49 | if (emitter.isDisposed()) return; 50 | BriteDatabase.Transaction transaction = mDb.newTransaction(); 51 | try { 52 | mDb.delete(Db.RibotProfileTable.TABLE_NAME, null); 53 | for (Ribot ribot : newRibots) { 54 | long result = mDb.insert(Db.RibotProfileTable.TABLE_NAME, 55 | Db.RibotProfileTable.toContentValues(ribot.profile()), 56 | SQLiteDatabase.CONFLICT_REPLACE); 57 | if (result >= 0) emitter.onNext(ribot); 58 | } 59 | transaction.markSuccessful(); 60 | emitter.onComplete(); 61 | } finally { 62 | transaction.end(); 63 | } 64 | } 65 | }); 66 | } 67 | 68 | public Observable> getRibots() { 69 | return mDb.createQuery(Db.RibotProfileTable.TABLE_NAME, 70 | "SELECT * FROM " + Db.RibotProfileTable.TABLE_NAME) 71 | .mapToList(new Function() { 72 | @Override 73 | public Ribot apply(@NonNull Cursor cursor) throws Exception { 74 | return Ribot.create(Db.RibotProfileTable.parseCursor(cursor)); 75 | } 76 | }); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/data/local/Db.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.data.local; 2 | 3 | import android.content.ContentValues; 4 | import android.database.Cursor; 5 | 6 | import java.util.Date; 7 | 8 | import uk.co.ribot.androidboilerplate.data.model.Name; 9 | import uk.co.ribot.androidboilerplate.data.model.Profile; 10 | 11 | public class Db { 12 | 13 | public Db() { } 14 | 15 | public abstract static class RibotProfileTable { 16 | public static final String TABLE_NAME = "ribot_profile"; 17 | 18 | public static final String COLUMN_EMAIL = "email"; 19 | public static final String COLUMN_FIRST_NAME = "first_name"; 20 | public static final String COLUMN_LAST_NAME = "last_name"; 21 | public static final String COLUMN_HEX_COLOR = "hex_color"; 22 | public static final String COLUMN_DATE_OF_BIRTH = "date_of_birth"; 23 | public static final String COLUMN_AVATAR = "avatar"; 24 | public static final String COLUMN_BIO = "bio"; 25 | 26 | public static final String CREATE = 27 | "CREATE TABLE " + TABLE_NAME + " (" + 28 | COLUMN_EMAIL + " TEXT PRIMARY KEY, " + 29 | COLUMN_FIRST_NAME + " TEXT NOT NULL, " + 30 | COLUMN_LAST_NAME + " TEXT NOT NULL, " + 31 | COLUMN_HEX_COLOR + " TEXT NOT NULL, " + 32 | COLUMN_DATE_OF_BIRTH + " INTEGER NOT NULL, " + 33 | COLUMN_AVATAR + " TEXT, " + 34 | COLUMN_BIO + " TEXT" + 35 | " ); "; 36 | 37 | public static ContentValues toContentValues(Profile profile) { 38 | ContentValues values = new ContentValues(); 39 | values.put(COLUMN_EMAIL, profile.email()); 40 | values.put(COLUMN_FIRST_NAME, profile.name().first()); 41 | values.put(COLUMN_LAST_NAME, profile.name().last()); 42 | values.put(COLUMN_HEX_COLOR, profile.hexColor()); 43 | values.put(COLUMN_DATE_OF_BIRTH, profile.dateOfBirth().getTime()); 44 | values.put(COLUMN_AVATAR, profile.avatar()); 45 | if (profile.bio() != null) values.put(COLUMN_BIO, profile.bio()); 46 | return values; 47 | } 48 | 49 | public static Profile parseCursor(Cursor cursor) { 50 | Name name = Name.create( 51 | cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_FIRST_NAME)), 52 | cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_LAST_NAME))); 53 | long dobTime = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_DATE_OF_BIRTH)); 54 | 55 | return Profile.builder() 56 | .setName(name) 57 | .setEmail(cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_EMAIL))) 58 | .setHexColor(cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_HEX_COLOR))) 59 | .setDateOfBirth(new Date(dobTime)) 60 | .setAvatar(cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_AVATAR))) 61 | .setBio(cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_BIO))) 62 | .build(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/data/local/DbOpenHelper.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.data.local; 2 | 3 | import android.content.Context; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.database.sqlite.SQLiteOpenHelper; 6 | 7 | import javax.inject.Inject; 8 | import javax.inject.Singleton; 9 | 10 | import uk.co.ribot.androidboilerplate.injection.ApplicationContext; 11 | 12 | @Singleton 13 | public class DbOpenHelper extends SQLiteOpenHelper { 14 | 15 | public static final String DATABASE_NAME = "ribots.db"; 16 | public static final int DATABASE_VERSION = 2; 17 | 18 | @Inject 19 | public DbOpenHelper(@ApplicationContext Context context) { 20 | super(context, DATABASE_NAME, null, DATABASE_VERSION); 21 | } 22 | 23 | @Override 24 | public void onConfigure(SQLiteDatabase db) { 25 | super.onConfigure(db); 26 | //Uncomment line below if you want to enable foreign keys 27 | //db.execSQL("PRAGMA foreign_keys=ON;"); 28 | } 29 | 30 | @Override 31 | public void onCreate(SQLiteDatabase db) { 32 | db.beginTransaction(); 33 | try { 34 | db.execSQL(Db.RibotProfileTable.CREATE); 35 | //Add other tables here 36 | db.setTransactionSuccessful(); 37 | } finally { 38 | db.endTransaction(); 39 | } 40 | } 41 | 42 | @Override 43 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/data/local/PreferencesHelper.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.data.local; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import javax.inject.Inject; 7 | import javax.inject.Singleton; 8 | 9 | import uk.co.ribot.androidboilerplate.injection.ApplicationContext; 10 | 11 | @Singleton 12 | public class PreferencesHelper { 13 | 14 | public static final String PREF_FILE_NAME = "android_boilerplate_pref_file"; 15 | 16 | private final SharedPreferences mPref; 17 | 18 | @Inject 19 | public PreferencesHelper(@ApplicationContext Context context) { 20 | mPref = context.getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE); 21 | } 22 | 23 | public void clear() { 24 | mPref.edit().clear().apply(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/data/model/Name.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.data.model; 2 | 3 | import android.os.Parcelable; 4 | 5 | import com.google.auto.value.AutoValue; 6 | import com.google.gson.Gson; 7 | import com.google.gson.TypeAdapter; 8 | 9 | 10 | @AutoValue 11 | public abstract class Name implements Parcelable { 12 | public abstract String first(); 13 | public abstract String last(); 14 | 15 | public static Name create(String first, String last) { 16 | return new AutoValue_Name(first, last); 17 | } 18 | 19 | public static TypeAdapter typeAdapter(Gson gson) { 20 | return new AutoValue_Name.GsonTypeAdapter(gson); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/data/model/Profile.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.data.model; 2 | 3 | import android.os.Parcelable; 4 | import android.support.annotation.Nullable; 5 | 6 | import com.google.auto.value.AutoValue; 7 | import com.google.gson.Gson; 8 | import com.google.gson.TypeAdapter; 9 | 10 | import java.util.Date; 11 | 12 | @AutoValue 13 | public abstract class Profile implements Parcelable { 14 | public abstract Name name(); 15 | public abstract String email(); 16 | public abstract String hexColor(); 17 | public abstract Date dateOfBirth(); 18 | @Nullable public abstract String bio(); 19 | @Nullable public abstract String avatar(); 20 | 21 | public static Builder builder() { 22 | return new AutoValue_Profile.Builder(); 23 | } 24 | 25 | public static TypeAdapter typeAdapter(Gson gson) { 26 | return new AutoValue_Profile.GsonTypeAdapter(gson); 27 | } 28 | 29 | @AutoValue.Builder 30 | public abstract static class Builder { 31 | public abstract Builder setName(Name name); 32 | public abstract Builder setEmail(String email); 33 | public abstract Builder setHexColor(String hexColor); 34 | public abstract Builder setDateOfBirth(Date dateOfBirth); 35 | public abstract Builder setBio(String bio); 36 | public abstract Builder setAvatar(String avatar); 37 | public abstract Profile build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/data/model/Ribot.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.data.model; 2 | 3 | import android.os.Parcelable; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.google.auto.value.AutoValue; 7 | import com.google.gson.Gson; 8 | import com.google.gson.TypeAdapter; 9 | 10 | @AutoValue 11 | public abstract class Ribot implements Comparable, Parcelable { 12 | 13 | public abstract Profile profile(); 14 | 15 | public static Ribot create(Profile profile) { 16 | return new AutoValue_Ribot(profile); 17 | } 18 | 19 | public static TypeAdapter typeAdapter(Gson gson) { 20 | return new AutoValue_Ribot.GsonTypeAdapter(gson); 21 | } 22 | 23 | @Override 24 | public int compareTo(@NonNull Ribot another) { 25 | return profile().name().first().compareToIgnoreCase(another.profile().name().first()); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/data/remote/RibotsService.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.data.remote; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | 6 | import java.util.List; 7 | 8 | import io.reactivex.Observable; 9 | import retrofit2.Retrofit; 10 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 11 | import retrofit2.converter.gson.GsonConverterFactory; 12 | import retrofit2.http.GET; 13 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 14 | import uk.co.ribot.androidboilerplate.util.MyGsonTypeAdapterFactory; 15 | 16 | public interface RibotsService { 17 | 18 | String ENDPOINT = "https://api.ribot.io/"; 19 | 20 | @GET("ribots") 21 | Observable> getRibots(); 22 | 23 | /******** Helper class that sets up a new services *******/ 24 | class Creator { 25 | 26 | public static RibotsService newRibotsService() { 27 | Gson gson = new GsonBuilder() 28 | .registerTypeAdapterFactory(MyGsonTypeAdapterFactory.create()) 29 | .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") 30 | .create(); 31 | Retrofit retrofit = new Retrofit.Builder() 32 | .baseUrl(RibotsService.ENDPOINT) 33 | .addConverterFactory(GsonConverterFactory.create(gson)) 34 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 35 | .build(); 36 | return retrofit.create(RibotsService.class); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/injection/ActivityContext.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Qualifier; 7 | 8 | @Qualifier 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface ActivityContext { 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/injection/ApplicationContext.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Qualifier; 7 | 8 | @Qualifier 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface ApplicationContext { 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/injection/ConfigPersistent.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Scope; 7 | 8 | import uk.co.ribot.androidboilerplate.injection.component.ConfigPersistentComponent; 9 | 10 | /** 11 | * A scoping annotation to permit dependencies conform to the life of the 12 | * {@link ConfigPersistentComponent} 13 | */ 14 | @Scope 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface ConfigPersistent { 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/injection/PerActivity.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.injection; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Scope; 7 | 8 | /** 9 | * A scoping annotation to permit objects whose lifetime should 10 | * conform to the life of the Activity to be memorised in the 11 | * correct component. 12 | */ 13 | @Scope 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface PerActivity { 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/injection/component/ActivityComponent.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.injection.component; 2 | 3 | import dagger.Subcomponent; 4 | import uk.co.ribot.androidboilerplate.injection.PerActivity; 5 | import uk.co.ribot.androidboilerplate.injection.module.ActivityModule; 6 | import uk.co.ribot.androidboilerplate.ui.main.MainActivity; 7 | 8 | /** 9 | * This component inject dependencies to all Activities across the application 10 | */ 11 | @PerActivity 12 | @Subcomponent(modules = ActivityModule.class) 13 | public interface ActivityComponent { 14 | 15 | void inject(MainActivity mainActivity); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/injection/component/ApplicationComponent.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.injection.component; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Component; 9 | import uk.co.ribot.androidboilerplate.data.DataManager; 10 | import uk.co.ribot.androidboilerplate.data.SyncService; 11 | import uk.co.ribot.androidboilerplate.data.local.DatabaseHelper; 12 | import uk.co.ribot.androidboilerplate.data.local.PreferencesHelper; 13 | import uk.co.ribot.androidboilerplate.data.remote.RibotsService; 14 | import uk.co.ribot.androidboilerplate.injection.ApplicationContext; 15 | import uk.co.ribot.androidboilerplate.injection.module.ApplicationModule; 16 | import uk.co.ribot.androidboilerplate.util.RxEventBus; 17 | 18 | @Singleton 19 | @Component(modules = ApplicationModule.class) 20 | public interface ApplicationComponent { 21 | 22 | void inject(SyncService syncService); 23 | 24 | @ApplicationContext Context context(); 25 | Application application(); 26 | RibotsService ribotsService(); 27 | PreferencesHelper preferencesHelper(); 28 | DatabaseHelper databaseHelper(); 29 | DataManager dataManager(); 30 | RxEventBus eventBus(); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/injection/component/ConfigPersistentComponent.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.injection.component; 2 | 3 | import dagger.Component; 4 | import uk.co.ribot.androidboilerplate.injection.ConfigPersistent; 5 | import uk.co.ribot.androidboilerplate.injection.module.ActivityModule; 6 | import uk.co.ribot.androidboilerplate.ui.base.BaseActivity; 7 | 8 | /** 9 | * A dagger component that will live during the lifecycle of an Activity but it won't 10 | * be destroy during configuration changes. Check {@link BaseActivity} to see how this components 11 | * survives configuration changes. 12 | * Use the {@link ConfigPersistent} scope to annotate dependencies that need to survive 13 | * configuration changes (for example Presenters). 14 | */ 15 | @ConfigPersistent 16 | @Component(dependencies = ApplicationComponent.class) 17 | public interface ConfigPersistentComponent { 18 | 19 | ActivityComponent activityComponent(ActivityModule activityModule); 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/injection/module/ActivityModule.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.injection.module; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | 6 | import dagger.Module; 7 | import dagger.Provides; 8 | import uk.co.ribot.androidboilerplate.injection.ActivityContext; 9 | 10 | @Module 11 | public class ActivityModule { 12 | 13 | private Activity mActivity; 14 | 15 | public ActivityModule(Activity activity) { 16 | mActivity = activity; 17 | } 18 | 19 | @Provides 20 | Activity provideActivity() { 21 | return mActivity; 22 | } 23 | 24 | @Provides 25 | @ActivityContext 26 | Context providesContext() { 27 | return mActivity; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/injection/module/ApplicationModule.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.injection.module; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | import uk.co.ribot.androidboilerplate.data.remote.RibotsService; 11 | import uk.co.ribot.androidboilerplate.injection.ApplicationContext; 12 | 13 | /** 14 | * Provide application-level dependencies. 15 | */ 16 | @Module 17 | public class ApplicationModule { 18 | protected final Application mApplication; 19 | 20 | public ApplicationModule(Application application) { 21 | mApplication = application; 22 | } 23 | 24 | @Provides 25 | Application provideApplication() { 26 | return mApplication; 27 | } 28 | 29 | @Provides 30 | @ApplicationContext 31 | Context provideContext() { 32 | return mApplication; 33 | } 34 | 35 | @Provides 36 | @Singleton 37 | RibotsService provideRibotsService() { 38 | return RibotsService.Creator.newRibotsService(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/ui/base/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.ui.base; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.util.LongSparseArray; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | import timber.log.Timber; 10 | import uk.co.ribot.androidboilerplate.BoilerplateApplication; 11 | import uk.co.ribot.androidboilerplate.injection.component.ActivityComponent; 12 | import uk.co.ribot.androidboilerplate.injection.component.ConfigPersistentComponent; 13 | import uk.co.ribot.androidboilerplate.injection.component.DaggerConfigPersistentComponent; 14 | import uk.co.ribot.androidboilerplate.injection.module.ActivityModule; 15 | 16 | /** 17 | * Abstract activity that every other Activity in this application must implement. It handles 18 | * creation of Dagger components and makes sure that instances of ConfigPersistentComponent survive 19 | * across configuration changes. 20 | */ 21 | public class BaseActivity extends AppCompatActivity { 22 | 23 | private static final String KEY_ACTIVITY_ID = "KEY_ACTIVITY_ID"; 24 | private static final AtomicLong NEXT_ID = new AtomicLong(0); 25 | private static final LongSparseArray 26 | sComponentsMap = new LongSparseArray<>(); 27 | 28 | private ActivityComponent mActivityComponent; 29 | private long mActivityId; 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | 35 | // Create the ActivityComponent and reuses cached ConfigPersistentComponent if this is 36 | // being called after a configuration change. 37 | mActivityId = savedInstanceState != null ? 38 | savedInstanceState.getLong(KEY_ACTIVITY_ID) : NEXT_ID.getAndIncrement(); 39 | 40 | ConfigPersistentComponent configPersistentComponent = sComponentsMap.get(mActivityId, null); 41 | 42 | if (configPersistentComponent == null) { 43 | Timber.i("Creating new ConfigPersistentComponent id=%d", mActivityId); 44 | configPersistentComponent = DaggerConfigPersistentComponent.builder() 45 | .applicationComponent(BoilerplateApplication.get(this).getComponent()) 46 | .build(); 47 | sComponentsMap.put(mActivityId, configPersistentComponent); 48 | } 49 | mActivityComponent = configPersistentComponent.activityComponent(new ActivityModule(this)); 50 | } 51 | 52 | @Override 53 | protected void onSaveInstanceState(Bundle outState) { 54 | super.onSaveInstanceState(outState); 55 | outState.putLong(KEY_ACTIVITY_ID, mActivityId); 56 | } 57 | 58 | @Override 59 | protected void onDestroy() { 60 | if (!isChangingConfigurations()) { 61 | Timber.i("Clearing ConfigPersistentComponent id=%d", mActivityId); 62 | sComponentsMap.remove(mActivityId); 63 | } 64 | super.onDestroy(); 65 | } 66 | 67 | public ActivityComponent activityComponent() { 68 | return mActivityComponent; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/ui/base/BasePresenter.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.ui.base; 2 | 3 | /** 4 | * Base class that implements the Presenter interface and provides a base implementation for 5 | * attachView() and detachView(). It also handles keeping a reference to the mvpView that 6 | * can be accessed from the children classes by calling getMvpView(). 7 | */ 8 | public class BasePresenter implements Presenter { 9 | 10 | private T mMvpView; 11 | 12 | @Override 13 | public void attachView(T mvpView) { 14 | mMvpView = mvpView; 15 | } 16 | 17 | @Override 18 | public void detachView() { 19 | mMvpView = null; 20 | } 21 | 22 | public boolean isViewAttached() { 23 | return mMvpView != null; 24 | } 25 | 26 | public T getMvpView() { 27 | return mMvpView; 28 | } 29 | 30 | public void checkViewAttached() { 31 | if (!isViewAttached()) throw new MvpViewNotAttachedException(); 32 | } 33 | 34 | public static class MvpViewNotAttachedException extends RuntimeException { 35 | public MvpViewNotAttachedException() { 36 | super("Please call Presenter.attachView(MvpView) before" + 37 | " requesting data to the Presenter"); 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/ui/base/MvpView.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.ui.base; 2 | 3 | 4 | /** 5 | * Base interface that any class that wants to act as a View in the MVP (Model View Presenter) 6 | * pattern must implement. Generally this interface will be extended by a more specific interface 7 | * that then usually will be implemented by an Activity or Fragment. 8 | */ 9 | public interface MvpView { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/ui/base/Presenter.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.ui.base; 2 | 3 | /** 4 | * Every presenter in the app must either implement this interface or extend BasePresenter 5 | * indicating the MvpView type that wants to be attached with. 6 | */ 7 | public interface Presenter { 8 | 9 | void attachView(V mvpView); 10 | 11 | void detachView(); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/ui/main/MainActivity.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.ui.main; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.widget.Toast; 9 | 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import javax.inject.Inject; 14 | 15 | import butterknife.BindView; 16 | import butterknife.ButterKnife; 17 | import uk.co.ribot.androidboilerplate.R; 18 | import uk.co.ribot.androidboilerplate.data.SyncService; 19 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 20 | import uk.co.ribot.androidboilerplate.ui.base.BaseActivity; 21 | import uk.co.ribot.androidboilerplate.util.DialogFactory; 22 | 23 | public class MainActivity extends BaseActivity implements MainMvpView { 24 | 25 | private static final String EXTRA_TRIGGER_SYNC_FLAG = 26 | "uk.co.ribot.androidboilerplate.ui.main.MainActivity.EXTRA_TRIGGER_SYNC_FLAG"; 27 | 28 | @Inject MainPresenter mMainPresenter; 29 | @Inject RibotsAdapter mRibotsAdapter; 30 | 31 | @BindView(R.id.recycler_view) RecyclerView mRecyclerView; 32 | 33 | /** 34 | * Return an Intent to start this Activity. 35 | * triggerDataSyncOnCreate allows disabling the background sync service onCreate. Should 36 | * only be set to false during testing. 37 | */ 38 | public static Intent getStartIntent(Context context, boolean triggerDataSyncOnCreate) { 39 | Intent intent = new Intent(context, MainActivity.class); 40 | intent.putExtra(EXTRA_TRIGGER_SYNC_FLAG, triggerDataSyncOnCreate); 41 | return intent; 42 | } 43 | 44 | @Override 45 | protected void onCreate(Bundle savedInstanceState) { 46 | super.onCreate(savedInstanceState); 47 | activityComponent().inject(this); 48 | setContentView(R.layout.activity_main); 49 | ButterKnife.bind(this); 50 | 51 | mRecyclerView.setAdapter(mRibotsAdapter); 52 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 53 | mMainPresenter.attachView(this); 54 | mMainPresenter.loadRibots(); 55 | 56 | if (getIntent().getBooleanExtra(EXTRA_TRIGGER_SYNC_FLAG, true)) { 57 | startService(SyncService.getStartIntent(this)); 58 | } 59 | } 60 | 61 | @Override 62 | protected void onDestroy() { 63 | super.onDestroy(); 64 | 65 | mMainPresenter.detachView(); 66 | } 67 | 68 | /***** MVP View methods implementation *****/ 69 | 70 | @Override 71 | public void showRibots(List ribots) { 72 | mRibotsAdapter.setRibots(ribots); 73 | mRibotsAdapter.notifyDataSetChanged(); 74 | } 75 | 76 | @Override 77 | public void showError() { 78 | DialogFactory.createGenericErrorDialog(this, getString(R.string.error_loading_ribots)) 79 | .show(); 80 | } 81 | 82 | @Override 83 | public void showRibotsEmpty() { 84 | mRibotsAdapter.setRibots(Collections.emptyList()); 85 | mRibotsAdapter.notifyDataSetChanged(); 86 | Toast.makeText(this, R.string.empty_ribots, Toast.LENGTH_LONG).show(); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/ui/main/MainMvpView.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.ui.main; 2 | 3 | import java.util.List; 4 | 5 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 6 | import uk.co.ribot.androidboilerplate.ui.base.MvpView; 7 | 8 | public interface MainMvpView extends MvpView { 9 | 10 | void showRibots(List ribots); 11 | 12 | void showRibotsEmpty(); 13 | 14 | void showError(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/ui/main/MainPresenter.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.ui.main; 2 | 3 | import java.util.List; 4 | 5 | import javax.inject.Inject; 6 | 7 | import io.reactivex.Observer; 8 | import io.reactivex.android.schedulers.AndroidSchedulers; 9 | import io.reactivex.annotations.NonNull; 10 | import io.reactivex.disposables.Disposable; 11 | import io.reactivex.schedulers.Schedulers; 12 | import timber.log.Timber; 13 | import uk.co.ribot.androidboilerplate.data.DataManager; 14 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 15 | import uk.co.ribot.androidboilerplate.injection.ConfigPersistent; 16 | import uk.co.ribot.androidboilerplate.ui.base.BasePresenter; 17 | import uk.co.ribot.androidboilerplate.util.RxUtil; 18 | 19 | @ConfigPersistent 20 | public class MainPresenter extends BasePresenter { 21 | 22 | private final DataManager mDataManager; 23 | private Disposable mDisposable; 24 | 25 | @Inject 26 | public MainPresenter(DataManager dataManager) { 27 | mDataManager = dataManager; 28 | } 29 | 30 | @Override 31 | public void attachView(MainMvpView mvpView) { 32 | super.attachView(mvpView); 33 | } 34 | 35 | @Override 36 | public void detachView() { 37 | super.detachView(); 38 | if (mDisposable != null) mDisposable.dispose(); 39 | } 40 | 41 | public void loadRibots() { 42 | checkViewAttached(); 43 | RxUtil.dispose(mDisposable); 44 | mDataManager.getRibots() 45 | .observeOn(AndroidSchedulers.mainThread()) 46 | .subscribeOn(Schedulers.io()) 47 | .subscribe(new Observer>() { 48 | @Override 49 | public void onSubscribe(@NonNull Disposable d) { 50 | mDisposable = d; 51 | } 52 | 53 | @Override 54 | public void onNext(@NonNull List ribots) { 55 | if (ribots.isEmpty()) { 56 | getMvpView().showRibotsEmpty(); 57 | } else { 58 | getMvpView().showRibots(ribots); 59 | } 60 | } 61 | 62 | @Override 63 | public void onError(@NonNull Throwable e) { 64 | Timber.e(e, "There was an error loading the ribots."); 65 | getMvpView().showError(); 66 | } 67 | 68 | @Override 69 | public void onComplete() { 70 | 71 | } 72 | }); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/ui/main/RibotsAdapter.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.ui.main; 2 | 3 | import android.graphics.Color; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import javax.inject.Inject; 14 | 15 | import butterknife.BindView; 16 | import butterknife.ButterKnife; 17 | import uk.co.ribot.androidboilerplate.R; 18 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 19 | 20 | public class RibotsAdapter extends RecyclerView.Adapter { 21 | 22 | private List mRibots; 23 | 24 | @Inject 25 | public RibotsAdapter() { 26 | mRibots = new ArrayList<>(); 27 | } 28 | 29 | public void setRibots(List ribots) { 30 | mRibots = ribots; 31 | } 32 | 33 | @Override 34 | public RibotViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 35 | View itemView = LayoutInflater.from(parent.getContext()) 36 | .inflate(R.layout.item_ribot, parent, false); 37 | return new RibotViewHolder(itemView); 38 | } 39 | 40 | @Override 41 | public void onBindViewHolder(final RibotViewHolder holder, int position) { 42 | Ribot ribot = mRibots.get(position); 43 | holder.hexColorView.setBackgroundColor(Color.parseColor(ribot.profile().hexColor())); 44 | holder.nameTextView.setText(String.format("%s %s", 45 | ribot.profile().name().first(), ribot.profile().name().last())); 46 | holder.emailTextView.setText(ribot.profile().email()); 47 | } 48 | 49 | @Override 50 | public int getItemCount() { 51 | return mRibots.size(); 52 | } 53 | 54 | class RibotViewHolder extends RecyclerView.ViewHolder { 55 | 56 | @BindView(R.id.view_hex_color) View hexColorView; 57 | @BindView(R.id.text_name) TextView nameTextView; 58 | @BindView(R.id.text_email) TextView emailTextView; 59 | 60 | public RibotViewHolder(View itemView) { 61 | super(itemView); 62 | ButterKnife.bind(this, itemView); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/util/AndroidComponentUtil.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | import android.app.ActivityManager; 4 | import android.app.ActivityManager.RunningServiceInfo; 5 | import android.content.ComponentName; 6 | import android.content.Context; 7 | import android.content.pm.PackageManager; 8 | 9 | public final class AndroidComponentUtil { 10 | 11 | public static void toggleComponent(Context context, Class componentClass, boolean enable) { 12 | ComponentName componentName = new ComponentName(context, componentClass); 13 | PackageManager pm = context.getPackageManager(); 14 | pm.setComponentEnabledSetting(componentName, 15 | enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : 16 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 17 | PackageManager.DONT_KILL_APP); 18 | } 19 | 20 | public static boolean isServiceRunning(Context context, Class serviceClass) { 21 | ActivityManager manager = 22 | (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 23 | for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { 24 | if (serviceClass.getName().equals(service.service.getClassName())) { 25 | return true; 26 | } 27 | } 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/util/DialogFactory.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | import android.app.Dialog; 4 | import android.app.ProgressDialog; 5 | import android.content.Context; 6 | import android.support.annotation.StringRes; 7 | import android.support.v7.app.AlertDialog; 8 | 9 | import uk.co.ribot.androidboilerplate.R; 10 | 11 | public final class DialogFactory { 12 | 13 | public static Dialog createSimpleOkErrorDialog(Context context, String title, String message) { 14 | AlertDialog.Builder alertDialog = new AlertDialog.Builder(context) 15 | .setTitle(title) 16 | .setMessage(message) 17 | .setNeutralButton(R.string.dialog_action_ok, null); 18 | return alertDialog.create(); 19 | } 20 | 21 | public static Dialog createSimpleOkErrorDialog(Context context, 22 | @StringRes int titleResource, 23 | @StringRes int messageResource) { 24 | 25 | return createSimpleOkErrorDialog(context, 26 | context.getString(titleResource), 27 | context.getString(messageResource)); 28 | } 29 | 30 | public static Dialog createGenericErrorDialog(Context context, String message) { 31 | AlertDialog.Builder alertDialog = new AlertDialog.Builder(context) 32 | .setTitle(context.getString(R.string.dialog_error_title)) 33 | .setMessage(message) 34 | .setNeutralButton(R.string.dialog_action_ok, null); 35 | return alertDialog.create(); 36 | } 37 | 38 | public static Dialog createGenericErrorDialog(Context context, @StringRes int messageResource) { 39 | return createGenericErrorDialog(context, context.getString(messageResource)); 40 | } 41 | 42 | public static ProgressDialog createProgressDialog(Context context, String message) { 43 | ProgressDialog progressDialog = new ProgressDialog(context); 44 | progressDialog.setMessage(message); 45 | return progressDialog; 46 | } 47 | 48 | public static ProgressDialog createProgressDialog(Context context, 49 | @StringRes int messageResource) { 50 | return createProgressDialog(context, context.getString(messageResource)); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/util/MyGsonTypeAdapterFactory.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | import com.google.gson.TypeAdapterFactory; 4 | import com.ryanharter.auto.value.gson.GsonTypeAdapterFactory; 5 | 6 | @GsonTypeAdapterFactory 7 | public abstract class MyGsonTypeAdapterFactory implements TypeAdapterFactory { 8 | public static TypeAdapterFactory create() { 9 | return new AutoValueGson_MyGsonTypeAdapterFactory(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/util/NetworkUtil.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.NetworkInfo; 6 | 7 | import retrofit2.HttpException; 8 | 9 | public class NetworkUtil { 10 | 11 | /** 12 | * Returns true if the Throwable is an instance of RetrofitError with an 13 | * http status code equals to the given one. 14 | */ 15 | public static boolean isHttpStatusCode(Throwable throwable, int statusCode) { 16 | return throwable instanceof HttpException 17 | && ((HttpException) throwable).code() == statusCode; 18 | } 19 | 20 | public static boolean isNetworkConnected(Context context) { 21 | ConnectivityManager cm = 22 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 23 | NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); 24 | return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/util/RxEventBus.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Singleton; 5 | 6 | import io.reactivex.BackpressureStrategy; 7 | import io.reactivex.Flowable; 8 | import io.reactivex.subjects.PublishSubject; 9 | 10 | /** 11 | * A simple event bus built with RxJava 12 | */ 13 | @Singleton 14 | public class RxEventBus { 15 | 16 | private final BackpressureStrategy mBackpressureStrategy = BackpressureStrategy.BUFFER; 17 | private final PublishSubject mBusSubject; 18 | 19 | @Inject 20 | public RxEventBus() { 21 | mBusSubject = PublishSubject.create(); 22 | } 23 | 24 | /** 25 | * Posts an object (usually an Event) to the bus 26 | */ 27 | public void post(Object event) { 28 | mBusSubject.onNext(event); 29 | } 30 | 31 | /** 32 | * Observable that will emmit everything posted to the event bus. 33 | */ 34 | public Flowable observable() { 35 | return mBusSubject.toFlowable(mBackpressureStrategy); 36 | } 37 | 38 | /** 39 | * Observable that only emits events of a specific class. 40 | * Use this if you only want to subscribe to one type of events. 41 | */ 42 | public Flowable filteredObservable(final Class eventClass) { 43 | return mBusSubject.ofType(eventClass).toFlowable(mBackpressureStrategy); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/util/RxUtil.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | import io.reactivex.disposables.Disposable; 4 | 5 | public class RxUtil { 6 | 7 | public static void dispose(Disposable disposable) { 8 | if (disposable != null && !disposable.isDisposed()) { 9 | disposable.dispose(); 10 | } 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/ribot/androidboilerplate/util/ViewUtil.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.res.Resources; 6 | import android.view.inputmethod.InputMethodManager; 7 | 8 | public final class ViewUtil { 9 | 10 | public static float pxToDp(float px) { 11 | float densityDpi = Resources.getSystem().getDisplayMetrics().densityDpi; 12 | return px / (densityDpi / 160f); 13 | } 14 | 15 | public static int dpToPx(int dp) { 16 | float density = Resources.getSystem().getDisplayMetrics().density; 17 | return Math.round(dp * density); 18 | } 19 | 20 | public static void hideKeyboard(Activity activity) { 21 | InputMethodManager imm = 22 | (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); 23 | imm.hideSoftInputFromWindow(activity.getWindow().getDecorView().getWindowToken(), 0); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_ribot.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 18 | 19 | 25 | 26 | 33 | 34 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ribot/android-boilerplate/74f576f9da0c7cc16ccd88221d443aa7d02a9ded/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ribot/android-boilerplate/74f576f9da0c7cc16ccd88221d443aa7d02a9ded/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ribot/android-boilerplate/74f576f9da0c7cc16ccd88221d443aa7d02a9ded/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ribot/android-boilerplate/74f576f9da0c7cc16ccd88221d443aa7d02a9ded/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #222222 4 | #000000 5 | #FFFFFF 6 | #ffdfdfdf 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | 24sp 7 | 22sp 8 | 20sp 9 | 18sp 10 | 16sp 11 | 14sp 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidBoilerPlate 3 | 4 | Settings 5 | 6 | OK 7 | Cancel 8 | Sorry 9 | 10 | There aren\'t any ribots 11 | There was a problem loading the ribots 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/test/java/uk/co/ribot/androidboilerplate/DataManagerTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.ArgumentMatchers; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.MockitoJUnitRunner; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | import io.reactivex.Observable; 14 | import io.reactivex.observers.TestObserver; 15 | import uk.co.ribot.androidboilerplate.data.DataManager; 16 | import uk.co.ribot.androidboilerplate.data.local.DatabaseHelper; 17 | import uk.co.ribot.androidboilerplate.data.local.PreferencesHelper; 18 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 19 | import uk.co.ribot.androidboilerplate.data.remote.RibotsService; 20 | import uk.co.ribot.androidboilerplate.test.common.TestDataFactory; 21 | 22 | import static org.mockito.Mockito.never; 23 | import static org.mockito.Mockito.verify; 24 | import static org.mockito.Mockito.when; 25 | 26 | /** 27 | * This test class performs local unit tests without dependencies on the Android framework 28 | * For testing methods in the DataManager follow this approach: 29 | * 1. Stub mock helper classes that your method relies on. e.g. RetrofitServices or DatabaseHelper 30 | * 2. Test the Observable using TestSubscriber 31 | * 3. Optionally write a SEPARATE test that verifies that your method is calling the right helper 32 | * using Mockito.verify() 33 | */ 34 | @RunWith(MockitoJUnitRunner.class) 35 | public class DataManagerTest { 36 | 37 | @Mock DatabaseHelper mMockDatabaseHelper; 38 | @Mock PreferencesHelper mMockPreferencesHelper; 39 | @Mock RibotsService mMockRibotsService; 40 | private DataManager mDataManager; 41 | 42 | @Before 43 | public void setUp() { 44 | mDataManager = new DataManager(mMockRibotsService, mMockPreferencesHelper, 45 | mMockDatabaseHelper); 46 | } 47 | 48 | @Test 49 | public void syncRibotsEmitsValues() { 50 | List ribots = Arrays.asList(TestDataFactory.makeRibot("r1"), 51 | TestDataFactory.makeRibot("r2")); 52 | stubSyncRibotsHelperCalls(ribots); 53 | 54 | TestObserver result = new TestObserver<>(); 55 | mDataManager.syncRibots().subscribe(result); 56 | result.assertNoErrors(); 57 | result.assertValueSequence(ribots); 58 | } 59 | 60 | @Test 61 | public void syncRibotsCallsApiAndDatabase() { 62 | List ribots = Arrays.asList(TestDataFactory.makeRibot("r1"), 63 | TestDataFactory.makeRibot("r2")); 64 | stubSyncRibotsHelperCalls(ribots); 65 | 66 | mDataManager.syncRibots().subscribe(); 67 | // Verify right calls to helper methods 68 | verify(mMockRibotsService).getRibots(); 69 | verify(mMockDatabaseHelper).setRibots(ribots); 70 | } 71 | 72 | @Test 73 | public void syncRibotsDoesNotCallDatabaseWhenApiFails() { 74 | when(mMockRibotsService.getRibots()) 75 | .thenReturn(Observable.>error(new RuntimeException())); 76 | 77 | mDataManager.syncRibots().subscribe(new TestObserver()); 78 | // Verify right calls to helper methods 79 | verify(mMockRibotsService).getRibots(); 80 | verify(mMockDatabaseHelper, never()).setRibots(ArgumentMatchers.anyList()); 81 | } 82 | 83 | private void stubSyncRibotsHelperCalls(List ribots) { 84 | // Stub calls to the ribot service and database helper. 85 | when(mMockRibotsService.getRibots()) 86 | .thenReturn(Observable.just(ribots)); 87 | when(mMockDatabaseHelper.setRibots(ribots)) 88 | .thenReturn(Observable.fromIterable(ribots)); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /app/src/test/java/uk/co/ribot/androidboilerplate/DatabaseHelperTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate; 2 | 3 | import android.database.Cursor; 4 | 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.robolectric.RobolectricTestRunner; 10 | import org.robolectric.RuntimeEnvironment; 11 | import org.robolectric.annotation.Config; 12 | 13 | import java.util.Arrays; 14 | import java.util.List; 15 | 16 | import io.reactivex.observers.TestObserver; 17 | import uk.co.ribot.androidboilerplate.data.local.DatabaseHelper; 18 | import uk.co.ribot.androidboilerplate.data.local.Db; 19 | import uk.co.ribot.androidboilerplate.data.local.DbOpenHelper; 20 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 21 | import uk.co.ribot.androidboilerplate.test.common.TestDataFactory; 22 | import uk.co.ribot.androidboilerplate.util.DefaultConfig; 23 | import uk.co.ribot.androidboilerplate.util.RxSchedulersOverrideRule; 24 | 25 | import static junit.framework.Assert.assertEquals; 26 | 27 | /** 28 | * Unit tests integration with a SQLite Database using Robolectric 29 | */ 30 | @RunWith(RobolectricTestRunner.class) 31 | @Config(constants = BuildConfig.class, sdk = DefaultConfig.EMULATE_SDK) 32 | public class DatabaseHelperTest { 33 | 34 | @Rule 35 | public final RxSchedulersOverrideRule mOverrideSchedulersRule = new RxSchedulersOverrideRule(); 36 | 37 | private DatabaseHelper mDatabaseHelper; 38 | 39 | @Before 40 | public void setup() { 41 | if (mDatabaseHelper == null) 42 | mDatabaseHelper = new DatabaseHelper(new DbOpenHelper(RuntimeEnvironment.application), 43 | mOverrideSchedulersRule.getScheduler()); 44 | } 45 | 46 | @Test 47 | public void setRibots() { 48 | Ribot ribot1 = TestDataFactory.makeRibot("r1"); 49 | Ribot ribot2 = TestDataFactory.makeRibot("r2"); 50 | List ribots = Arrays.asList(ribot1, ribot2); 51 | 52 | TestObserver result = new TestObserver<>(); 53 | mDatabaseHelper.setRibots(ribots).subscribe(result); 54 | result.assertNoErrors(); 55 | result.assertValueSequence(ribots); 56 | 57 | Cursor cursor = mDatabaseHelper.getBriteDb() 58 | .query("SELECT * FROM " + Db.RibotProfileTable.TABLE_NAME); 59 | assertEquals(2, cursor.getCount()); 60 | for (Ribot ribot : ribots) { 61 | cursor.moveToNext(); 62 | assertEquals(ribot.profile(), Db.RibotProfileTable.parseCursor(cursor)); 63 | } 64 | } 65 | 66 | @Test 67 | public void getRibots() { 68 | Ribot ribot1 = TestDataFactory.makeRibot("r1"); 69 | Ribot ribot2 = TestDataFactory.makeRibot("r2"); 70 | List ribots = Arrays.asList(ribot1, ribot2); 71 | 72 | mDatabaseHelper.setRibots(ribots).subscribe(); 73 | 74 | TestObserver> result = new TestObserver<>(); 75 | mDatabaseHelper.getRibots().subscribe(result); 76 | result.assertNoErrors(); 77 | result.assertValue(ribots); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /app/src/test/java/uk/co/ribot/androidboilerplate/MainPresenterTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.ArgumentMatchers; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.MockitoJUnitRunner; 11 | 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | import io.reactivex.Observable; 16 | import uk.co.ribot.androidboilerplate.data.DataManager; 17 | import uk.co.ribot.androidboilerplate.data.model.Ribot; 18 | import uk.co.ribot.androidboilerplate.test.common.TestDataFactory; 19 | import uk.co.ribot.androidboilerplate.ui.main.MainMvpView; 20 | import uk.co.ribot.androidboilerplate.ui.main.MainPresenter; 21 | import uk.co.ribot.androidboilerplate.util.RxSchedulersOverrideRule; 22 | 23 | import static org.mockito.Mockito.never; 24 | import static org.mockito.Mockito.verify; 25 | import static org.mockito.Mockito.when; 26 | 27 | @RunWith(MockitoJUnitRunner.class) 28 | public class MainPresenterTest { 29 | 30 | @Mock MainMvpView mMockMainMvpView; 31 | @Mock DataManager mMockDataManager; 32 | private MainPresenter mMainPresenter; 33 | 34 | @Rule 35 | public final RxSchedulersOverrideRule mOverrideSchedulersRule = new RxSchedulersOverrideRule(); 36 | 37 | @Before 38 | public void setUp() { 39 | mMainPresenter = new MainPresenter(mMockDataManager); 40 | mMainPresenter.attachView(mMockMainMvpView); 41 | } 42 | 43 | @After 44 | public void tearDown() { 45 | mMainPresenter.detachView(); 46 | } 47 | 48 | @Test 49 | public void loadRibotsReturnsRibots() { 50 | List ribots = TestDataFactory.makeListRibots(10); 51 | when(mMockDataManager.getRibots()) 52 | .thenReturn(Observable.just(ribots)); 53 | 54 | mMainPresenter.loadRibots(); 55 | verify(mMockMainMvpView).showRibots(ribots); 56 | verify(mMockMainMvpView, never()).showRibotsEmpty(); 57 | verify(mMockMainMvpView, never()).showError(); 58 | } 59 | 60 | @Test 61 | public void loadRibotsReturnsEmptyList() { 62 | when(mMockDataManager.getRibots()) 63 | .thenReturn(Observable.just(Collections.emptyList())); 64 | 65 | mMainPresenter.loadRibots(); 66 | verify(mMockMainMvpView).showRibotsEmpty(); 67 | verify(mMockMainMvpView, never()).showRibots(ArgumentMatchers.anyList()); 68 | verify(mMockMainMvpView, never()).showError(); 69 | } 70 | 71 | @Test 72 | public void loadRibotsFails() { 73 | when(mMockDataManager.getRibots()) 74 | .thenReturn(Observable.>error(new RuntimeException())); 75 | 76 | mMainPresenter.loadRibots(); 77 | verify(mMockMainMvpView).showError(); 78 | verify(mMockMainMvpView, never()).showRibotsEmpty(); 79 | verify(mMockMainMvpView, never()).showRibots(ArgumentMatchers.anyList()); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/test/java/uk/co/ribot/androidboilerplate/util/DefaultConfig.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | /** 4 | * Robolectric default config properties 5 | */ 6 | public class DefaultConfig { 7 | //The api level that Roboelectric will use to run the unit tests 8 | public static final int EMULATE_SDK = 23; 9 | } -------------------------------------------------------------------------------- /app/src/test/java/uk/co/ribot/androidboilerplate/util/RxEventBusTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | import org.junit.Before; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | 7 | import io.reactivex.subscribers.TestSubscriber; 8 | 9 | public class RxEventBusTest { 10 | 11 | private RxEventBus mEventBus; 12 | 13 | @Rule 14 | // Must be added to every test class that targets app code that uses RxJava 15 | public final RxSchedulersOverrideRule mOverrideSchedulersRule = new RxSchedulersOverrideRule(); 16 | 17 | @Before 18 | public void setUp() { 19 | mEventBus = new RxEventBus(); 20 | } 21 | 22 | @Test 23 | public void postedObjectsAreReceived() { 24 | TestSubscriber testSubscriber = new TestSubscriber<>(); 25 | mEventBus.observable().subscribe(testSubscriber); 26 | 27 | Object event1 = new Object(); 28 | Object event2 = new Object(); 29 | mEventBus.post(event1); 30 | mEventBus.post(event2); 31 | 32 | testSubscriber.assertValues(event1, event2); 33 | } 34 | 35 | @Test 36 | public void filteredObservableOnlyReceivesSomeObjects() { 37 | TestSubscriber testSubscriber = new TestSubscriber<>(); 38 | mEventBus.filteredObservable(String.class).subscribe(testSubscriber); 39 | 40 | String stringEvent = "Event"; 41 | Integer intEvent = 20; 42 | mEventBus.post(stringEvent); 43 | mEventBus.post(intEvent); 44 | 45 | testSubscriber.assertValueCount(1); 46 | testSubscriber.assertValue(stringEvent); 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/test/java/uk/co/ribot/androidboilerplate/util/RxSchedulersOverrideRule.java: -------------------------------------------------------------------------------- 1 | package uk.co.ribot.androidboilerplate.util; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | import java.util.concurrent.Callable; 7 | 8 | import io.reactivex.Scheduler; 9 | import io.reactivex.android.plugins.RxAndroidPlugins; 10 | import io.reactivex.annotations.NonNull; 11 | import io.reactivex.functions.Function; 12 | import io.reactivex.plugins.RxJavaPlugins; 13 | import io.reactivex.schedulers.Schedulers; 14 | 15 | /** 16 | * This rule registers Handlers for RxJava and RxAndroid to ensure that subscriptions 17 | * always subscribeOn and observeOn Schedulers.trampoline(). 18 | * Warning, this rule will reset RxAndroidPlugins and RxJavaPlugins before and after each test so 19 | * if the application code uses RxJava plugins this may affect the behaviour of the testing method. 20 | */ 21 | public class RxSchedulersOverrideRule implements TestRule { 22 | 23 | private final Function, Scheduler> mRxAndroidSchedulersHook = 24 | new Function, Scheduler>() { 25 | @Override 26 | public Scheduler apply(@NonNull Callable schedulerCallable) 27 | throws Exception { 28 | return getScheduler(); 29 | } 30 | }; 31 | 32 | private final Function mRxJavaImmediateScheduler = 33 | new Function() { 34 | @Override 35 | public Scheduler apply(@NonNull Scheduler scheduler) throws Exception { 36 | return getScheduler(); 37 | } 38 | }; 39 | 40 | @Override 41 | public Statement apply(final Statement base, Description description) { 42 | return new Statement() { 43 | @Override 44 | public void evaluate() throws Throwable { 45 | RxAndroidPlugins.reset(); 46 | RxAndroidPlugins.setInitMainThreadSchedulerHandler(mRxAndroidSchedulersHook); 47 | 48 | RxJavaPlugins.reset(); 49 | RxJavaPlugins.setIoSchedulerHandler(mRxJavaImmediateScheduler); 50 | RxJavaPlugins.setNewThreadSchedulerHandler(mRxJavaImmediateScheduler); 51 | 52 | base.evaluate(); 53 | 54 | RxAndroidPlugins.reset(); 55 | RxJavaPlugins.reset(); 56 | } 57 | }; 58 | } 59 | 60 | public Scheduler getScheduler() { 61 | return Schedulers.trampoline(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | maven { url 'https://maven.fabric.io/public' } 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:2.3.2' 8 | classpath 'com.github.triplet.gradle:play-publisher:1.1.4' 9 | //noinspection GradleDynamicVersion 10 | classpath 'io.fabric.tools:gradle:1.+' 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | jcenter() 17 | maven { url 'https://maven.fabric.io/public' } 18 | } 19 | } 20 | 21 | task wrapper(type: Wrapper) { 22 | gradleVersion = '3.3' 23 | distributionUrl = distributionUrl.replace("bin", "all") 24 | } 25 | -------------------------------------------------------------------------------- /config/quality/checkstyle/checkstyle-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 73 | 75 | 77 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 88 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 113 | 114 | 115 | 116 | 117 | 119 | 120 | 121 | 122 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 133 | 134 | 135 | 136 | 137 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 147 | 148 | 149 | 150 | 151 | 153 | 154 | 155 | 156 | 157 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /config/quality/findbugs/android-exclude-filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /config/quality/pmd/pmd-ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | Custom ruleset for ribot Android application 8 | 9 | .*/R.java 10 | .*/gen/.* 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /config/quality/quality.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Set up Checkstyle, Findbugs and PMD to perform extensive code analysis. 3 | * 4 | * Gradle tasks added: 5 | * - checkstyle 6 | * - findbugs 7 | * - pmd 8 | * 9 | * The three tasks above are added as dependencies of the check task so running check will 10 | * run all of them. 11 | */ 12 | 13 | apply plugin: 'checkstyle' 14 | apply plugin: 'findbugs' 15 | apply plugin: 'pmd' 16 | 17 | dependencies { 18 | checkstyle 'com.puppycrawl.tools:checkstyle:8.7' 19 | } 20 | 21 | def qualityConfigDir = "$project.rootDir/config/quality"; 22 | def reportsDir = "$project.buildDir/reports" 23 | 24 | check.dependsOn 'checkstyle', 'findbugs', 'pmd' 25 | 26 | task checkstyle(type: Checkstyle, group: 'Verification', description: 'Runs code style checks') { 27 | configFile file("$qualityConfigDir/checkstyle/checkstyle-config.xml") 28 | source 'src' 29 | include '**/*.java' 30 | 31 | reports { 32 | xml.enabled = true 33 | xml { 34 | destination "$reportsDir/checkstyle/checkstyle.xml" 35 | } 36 | } 37 | 38 | classpath = files( ) 39 | } 40 | 41 | task findbugs(type: FindBugs, 42 | group: 'Verification', 43 | description: 'Inspect java bytecode for bugs', 44 | dependsOn: ['compileDebugSources','compileReleaseSources']) { 45 | 46 | ignoreFailures = false 47 | effort = "max" 48 | reportLevel = "high" 49 | excludeFilter = new File("$qualityConfigDir/findbugs/android-exclude-filter.xml") 50 | classes = files("$project.rootDir/app/build/intermediates/classes") 51 | 52 | source 'src' 53 | include '**/*.java' 54 | exclude '**/gen/**' 55 | 56 | reports { 57 | xml.enabled = true 58 | html.enabled = false 59 | xml { 60 | destination "$reportsDir/findbugs/findbugs.xml" 61 | } 62 | html { 63 | destination "$reportsDir/findbugs/findbugs.html" 64 | } 65 | } 66 | 67 | classpath = files() 68 | } 69 | 70 | 71 | task pmd(type: Pmd, group: 'Verification', description: 'Inspect sourcecode for bugs') { 72 | ruleSetFiles = files("$qualityConfigDir/pmd/pmd-ruleset.xml") 73 | ignoreFailures = false 74 | ruleSets = [] 75 | 76 | source 'src' 77 | include '**/*.java' 78 | exclude '**/gen/**' 79 | 80 | reports { 81 | xml.enabled = true 82 | html.enabled = true 83 | xml { 84 | destination "$reportsDir/pmd/pmd.xml" 85 | } 86 | html { 87 | destination "$reportsDir/pmd/pmd.html" 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /crashlytics_release_notes.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ribot/android-boilerplate/74f576f9da0c7cc16ccd88221d443aa7d02a9ded/crashlytics_release_notes.txt -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle performance variables 2 | org.gradle.jvmargs=-Xmx4G -XX:MaxPermSize=512m 3 | org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ribot/android-boilerplate/74f576f9da0c7cc16ccd88221d443aa7d02a9ded/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Sep 21 16:15:04 CST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /images/check-task-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ribot/android-boilerplate/74f576f9da0c7cc16ccd88221d443aa7d02a9ded/images/check-task-diagram.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------