├── .gitignore ├── .idea ├── runConfigurations.xml └── runConfigurations │ ├── All_tests_at_once.xml │ ├── App_and_modules_tests.xml │ ├── Base_tests.xml │ ├── Repositories_tests.xml │ └── Repository_tests.xml ├── .travis.yml ├── README.md ├── app ├── build.gradle ├── proguard-rules.pro ├── proguard-test-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── frogermcs │ │ └── multimodulegithubclient │ │ └── endtoend │ │ └── AppFlowTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── frogermcs │ │ │ └── multimodulegithubclient │ │ │ ├── App.java │ │ │ ├── AppComponent.java │ │ │ ├── AppComponentWrapper.java │ │ │ ├── AppScope.java │ │ │ ├── GithubClientModule.java │ │ │ ├── SplashActivity.java │ │ │ ├── SplashActivityComponent.java │ │ │ ├── SplashActivityModule.java │ │ │ └── SplashActivityPresenter.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_splash.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── frogermcs │ └── multimodulegithubclient │ └── SplashActivityPresenterTest.java ├── build.gradle ├── buildsystem ├── android_commons.gradle ├── dependencies.gradle └── jacoco.gradle ├── docs └── img │ ├── all_tests_sequential.png │ ├── app_diagram.png │ ├── as_run_configurations.png │ ├── coverage_report.png │ ├── dagger_diagram.png │ ├── failing_all_tests.png │ ├── instrumentation_report_example.png │ ├── no-proguard.png │ └── with-proguard.png ├── features ├── base │ ├── build.gradle │ ├── feature-base-proguard-rules.pro │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── frogermcs │ │ │ │ └── multimodulegithubclient │ │ │ │ └── base │ │ │ │ ├── ActivityScope.java │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── BaseComponent.java │ │ │ │ ├── BaseComponentWrapper.java │ │ │ │ ├── BaseModule.java │ │ │ │ ├── data │ │ │ │ ├── api │ │ │ │ │ ├── GithubApiService.java │ │ │ │ │ ├── NetworkingModule.java │ │ │ │ │ ├── RepositoriesManager.java │ │ │ │ │ ├── UserManager.java │ │ │ │ │ └── response │ │ │ │ │ │ ├── RepositoryResponse.java │ │ │ │ │ │ └── UserResponse.java │ │ │ │ └── model │ │ │ │ │ ├── Repository.java │ │ │ │ │ └── User.java │ │ │ │ └── utils │ │ │ │ ├── AnalyticsManager.java │ │ │ │ └── Validator.java │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── frogermcs │ │ └── multimodulegithubclient │ │ └── base │ │ └── ExampleUnitTest.java ├── repositories │ ├── build.gradle │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── frogermcs │ │ │ │ └── multimodulegithubclient │ │ │ │ └── repositories │ │ │ │ ├── RepositoriesFeatureComponent.java │ │ │ │ ├── RepositoriesFeatureComponentWrapper.java │ │ │ │ ├── RepositoriesFeatureScope.java │ │ │ │ ├── RepositoriesListActivity.java │ │ │ │ ├── RepositoriesListActivityComponent.java │ │ │ │ ├── RepositoriesListActivityModule.java │ │ │ │ ├── RepositoriesListActivityPresenter.java │ │ │ │ ├── RepositoriesListAdapter.java │ │ │ │ ├── RepositoriesListViewHolderFactory.java │ │ │ │ ├── RepositoriesModule.java │ │ │ │ ├── RepositoryViewHolder.java │ │ │ │ ├── RepositoryViewHolderBig.java │ │ │ │ ├── RepositoryViewHolderFeatured.java │ │ │ │ └── RepositoryViewHolderNormal.java │ │ └── res │ │ │ ├── layout │ │ │ ├── activity_repositories_list.xml │ │ │ ├── list_item_big.xml │ │ │ ├── list_item_featured.xml │ │ │ └── list_item_normal.xml │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── frogermcs │ │ └── multimodulegithubclient │ │ └── repositories │ │ └── RepositoriesListActivityPresenterTest.java └── repository │ ├── build.gradle │ └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── frogermcs │ │ │ └── multimodulegithubclient │ │ │ └── repository │ │ │ ├── RepositoryDetailsActivity.java │ │ │ ├── RepositoryDetailsActivityComponent.java │ │ │ ├── RepositoryDetailsActivityModule.java │ │ │ ├── RepositoryDetailsActivityPresenter.java │ │ │ ├── RepositoryFeatureComponent.java │ │ │ ├── RepositoryFeatureComponentWrapper.java │ │ │ └── RepositoryFeatureScope.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_repository_details.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── frogermcs │ └── multimodulegithubclient │ └── repository │ └── RepositoryDetailsActivityPresenterTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/caches 6 | /.idea/codeStyles 7 | /.idea/markdown-navigator 8 | /.idea/modules.xml 9 | /.idea/workspace.xml 10 | /.idea/gradle.xml 11 | /.idea/markdown-navigator.xml 12 | /.idea/misc.xml 13 | /.idea/vcs.xml 14 | .DS_Store 15 | /build 16 | /captures 17 | .externalNativeBuild 18 | app/build 19 | features/base/build 20 | features/repositories/build 21 | features/repository/build 22 | jacoco.exec 23 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/runConfigurations/All_tests_at_once.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/runConfigurations/App_and_modules_tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Base_tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Repositories_tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Repository_tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | android: 4 | components: 5 | - build-tools-28.0.3 6 | - android-29 7 | 8 | - extra-google-m2repository 9 | - extra-android-m2repository 10 | licenses: 11 | - 'android-sdk-preview-license-52d11cd2' 12 | - 'android-sdk-license-.+' 13 | - 'google-gdk-license-.+' 14 | 15 | jdk: 16 | - oraclejdk8 17 | 18 | before_install: 19 | - yes | sdkmanager "build-tools;28.0.3" 20 | 21 | notifications: 22 | email: true 23 | 24 | cache: 25 | directories: 26 | - $HOME/.m2 27 | 28 | script: 29 | ./gradlew testDebugUnitTest testDebugUnitTestCoverage assembleDebug 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/frogermcs/MultiModuleGithubClient.svg?branch=master)](https://travis-ci.com/frogermcs/MultiModuleGithubClient) 2 | 3 | # MultiModuleGithubClient 4 | 5 | Breaking the monolith to microservices is a well-known concept to make backend solutions extendable and maintainable in a scale, by bigger teams. Since mobile apps have become more complex, very often developed by teams of tens of software engineers this concept also grows in mobile platforms. There are many benefits from having apps split into modules/features/libraries: 6 | 7 | * features can be developed independently 8 | * project structure is cleaner 9 | * building process can be way faster (e.g., running unit tests on a module can be a matter of seconds, instead of minutes for the entire project) 10 | * great starting point for [instant apps](https://developer.android.com/topic/google-play-instant/) 11 | 12 | This is the example project which resolves (or at least workarounds) the most common problems with the multi-module Android app. 13 | 14 | ## Project structure 15 | 16 | The project is a straightforward Github API client containing 3 screens (user search, repositories list, repository details). For the sake of simplicity each screen is a separate module: 17 | 18 | * app - application module containing main app screen (user search) 19 | * repositories - repositories list 20 | * repository - repository detail 21 | * base - module containing a code shared between all modules 22 | 23 | ![Project structure](docs/img/app_diagram.png "Project structure") 24 | 25 | ### Dependencies management 26 | It is easy to get lost with dependencies management across different project modules (especially with libs versions). To make it easier, take a look at `buildsystem/dependencies.gradle` where everything is configured. Each module has separate configuration, with additional two for testing and annotation processing. Like some other patterns, this was originally introduced in [Azimo](https://azimo.com) Android application by [@dbarwacz](https://github.com/dbarwacz). 27 | 28 | ## Dagger 2 29 | 30 | Recently there is no recommended Dagger 2 configuration for multi-module Android project. Some software engineers recommend exposing Dagger modules from Android feature module and use them in Components maintained only in the main App module. This project implements the second way - each feature module has its Component and dependencies tree. All of them depends on Base Component (created in Base module). All have their scope and subcomponents. 31 | This approach was proposed by my colleague [@dbarwacz](https://github.com/dbarwacz) and recently is heavily used in [Azimo](https://azimo.com) Android application. 32 | 33 | Here are some highlights from it: 34 | 35 | * `BaseComponent` is used as a dependency in feature components (e.g. `RepositoriesFeatureComponent`, `AppComponent`...). It means that all dependencies that are used in needs to be publicly exposed in `BaseComponent` interface. 36 | * Local components, like `SplashActivityComponent` are subcomponents of feature component (`SplashActivityComponent` is a subcomponent of `AppComponent`). 37 | * Each module has its own Scope (e.g. `RepositoryFeatureScope`, `AppScope`). Effectively they define singletons - they live as long as components, which are maintained by classes: `AppComponentWrapper`, `RepositoryFeatureComponentWrapper` which are singletons... . To have better control on Scopes lifecycle, a good idea would be to add `release()` method to ComponentWrappers. 38 | 39 | For the rest take a look at the code - should be self-explaining. As this is just self-invented setup (that works on production!), all kind of feedback is warmly welcomed. 40 | 41 | ![Dagger structue](docs/img/dagger_diagram.png "Dagger structure") 42 | 43 | ## Unit Testing 44 | 45 | Project contains some example unit tests for presenter classes. 46 | 47 | ### Gradle 48 | 49 | To run all unit tests from all modules at once execute: 50 | 51 | ``` 52 | ./gradlew testDebugUnitTest 53 | ``` 54 | 55 | In console you can see: 56 | 57 | ``` 58 | ... 59 | > Task :app:testDebugUnitTest 60 | com.frogermcs.multimodulegithubclient.SplashActivityPresenterTest > testNavigation_whenUserLoaded_thenShouldNavigateToRepositoriesList PASSED 61 | com.frogermcs.multimodulegithubclient.SplashActivityPresenterTest > testErrorHandling_whenErrorOccuredWhileLoadingUser_thenShouldShowValidationError PASSED 62 | com.frogermcs.multimodulegithubclient.SplashActivityPresenterTest > testValidation_whenUserNameIsInvalid_thenShouldShowValidationError PASSED 63 | com.frogermcs.multimodulegithubclient.SplashActivityPresenterTest > testValidation_whenUserNameValid_thenShouldLoadUser PASSED 64 | com.frogermcs.multimodulegithubclient.SplashActivityPresenterTest > testInit_shouldLogLaunchedScreenIntoAnalytics PASSED 65 | 66 | > Task :features:base:testDebugUnitTest 67 | com.frogermcs.multimodulegithubclient.base.ExampleUnitTest > addition_isCorrect PASSED 68 | 69 | > Task :features:repositories:testDebugUnitTest 70 | com.frogermcs.multimodulegithubclient.repositories.RepositoriesListActivityPresenterTest > testRepositories_whenRepositoriesAreLoaded_thenShouldBePresented PASSED 71 | com.frogermcs.multimodulegithubclient.repositories.RepositoriesListActivityPresenterTest > testInit_shouldLoadRepositoriesForGivenUser PASSED 72 | com.frogermcs.multimodulegithubclient.repositories.RepositoriesListActivityPresenterTest > testNavigation_whenRepositoryClicked_thenShouldLaunchRepositoryDetails PASSED 73 | com.frogermcs.multimodulegithubclient.repositories.RepositoriesListActivityPresenterTest > testInit_shouldLogLaunchedScreenIntoAnalytics PASSED 74 | 75 | > Task :features:repository:testDebugUnitTest 76 | com.frogermcs.multimodulegithubclient.repository.RepositoryDetailsActivityPresenterTest > testUnit_shouldSetRepositoryDetails PASSED 77 | com.frogermcs.multimodulegithubclient.repository.RepositoryDetailsActivityPresenterTest > testInit_shouldSetUserName PASSED 78 | com.frogermcs.multimodulegithubclient.repository.RepositoryDetailsActivityPresenterTest > testInit_shouldLogLaunchedScreenIntoAnalytics PASSED 79 | 80 | BUILD SUCCESSFUL in 55s 81 | ... 82 | ``` 83 | It can be useful especially when you run tests on your CI environment. To control logs take a look at file `buildsystem/android_commons.gradle` (it is included in all feature modules). 84 | 85 | ``` 86 | testOptions { 87 | unitTests { 88 | all { 89 | testLogging { 90 | events 'passed', 'skipped', 'failed' 91 | } 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | In case you want to run unit test in one module, execute: 98 | 99 | ``` 100 | ./gradlew clean app:testDebugUnitTest 101 | ``` 102 | or 103 | ``` 104 | /gradlew clean feature:repository:testDebugUnitTest 105 | ``` 106 | 107 | ### Android Studio 108 | 109 | The repository contains shared configurations for running Unit Tests directly from Android Studio. 110 | 111 | ![Android Studio configs](docs/img/as_run_configurations.png "Android Studio configs") 112 | 113 | #### All tests at once 114 | 115 | Recently it is not always possible to run all tests at once (see troubleshooting below). 116 | 117 | #### All tests, module after module sequentially 118 | 119 | Run `App and modules tests`. This configuration will run unit tests from all modules in separate tabs, one after another. To modify list of tests, click on `Edit Configurations` -> `App and modules tests` -> `Before Launch`. 120 | 121 | ![All tests sequential](docs/img/all_tests_sequential.png "All tests sequential") 122 | 123 | #### Troubleshooting 124 | 125 | * **Class not found: ... Empty test suite.** 126 | There is a bug in Android Studio which prevents from launching all unit tests at once, before their code is generated (what happens after the first run of unit tests for every single module independently). For more take a look at Android Studio bug tracker: https://issuetracker.google.com/issues/111154138. 127 | 128 | ![Android Studio issues](docs/img/failing_all_tests.png "Android Studio issues") 129 | 130 | ## Test coverage for unit tests 131 | 132 | The project contains additional configuration for Jacoco that enables coverage report for Unit Tests (initially Jacoco reports cover Android Instrumentation Tests). 133 | 134 | To run tests coverage, execute: 135 | 136 | ``` 137 | ./gradlew testDebugUnitTestCoverage 138 | ``` 139 | 140 | Coverage report can be found in `app/build/reports/jacoco/testDebugUnitTestCoverage/html/index.html` (there is also an .xml file in case you would like to integrate coverage report with CI/CD environment. 141 | 142 | ### Implementation details 143 | 144 | Setting up a coverage report for Android Project isn't still straightforward and can take a couple of hours/days of exploration. Example setup in this project could be a little bit easier and more elegant, but some solutions are coded explicitly for better clarity. 145 | Here are some highlights: 146 | 147 | * Each module should use Jacoco plugin `apply plugin: 'jacoco'` and config (defined in `android_commons.gradle`): 148 | ``` 149 | buildTypes { 150 | debug { 151 | testCoverageEnabled true 152 | } 153 | } 154 | ``` 155 | 156 | * App module defines custom Jacoco task called `testDebugUnitTestCoverage`. Entire configuration can be found in `buildsystem/jacoco.gradle`. The code should be self-explaining. 157 | 158 | * Task `testDebugUnitTestCoverage` depends on `testDebugUnitTest` tasks (each module separately). Thanks to it all sources required for coverage report are available before gradle starts generating it (in `/build/...`. 159 | 160 | ![Coverage report](docs/img/coverage_report.png "Coverage report") 161 | 162 | 163 | ## Instrumentation Testing 164 | 165 | Project contains example Instrumentation test. 166 | 167 | ### Gradle 168 | 169 | To run all Instrumentation tests from all modules at once launch emulator or plugin device and execute: 170 | 171 | ``` 172 | ./gradlew connectedAndroidTest 173 | ``` 174 | 175 | When all goes fine, you should see testing report in `app/build/reports/androidTests/connected/` directory. 176 | ![Instrumentation test report](docs/img/instrumentation_report_example.png "Instrumentation test report") 177 | 178 | ### Functional vs End-to-end testing 179 | From the high level, Android Instrumentation tests can be split into two types: functional and end-to-end. You can check my [article](https://medium.com/azimolabs/automated-testing-will-set-your-engineering-team-free-a89467c40731) about how we do QA at Azimo to see what is the difference between both of them. 180 | 181 | Having in mind multi-feature config, it's pretty likely building functional tests can be more difficult. It's because your modules won't always see each other. In our example `app` module doesn't have knowledge about `feature/repository` module, so it means that instead of code: 182 | 183 | ```java 184 | intended(hasComponent(RepositoryDetailsActivity.class.getName())); 185 | ``` 186 | 187 | you need to use: 188 | 189 | ```java 190 | intended(hasComponent("com.frogermcs.multimodulegithubclient.repository.RepositoryDetailsActivity")); 191 | ``` 192 | 193 | It is, because `app` module doesn't have access to `RepositoryDetailsActivity` class. 194 | 195 | What about end-to-end tests? They shouldn't be problematic, simply because tests shouldn't have knowledge about specific implementation, but rather how user interface is composed (so again, not: `withText("R.string.show_repos")` but `withText("Show repositories")`). 196 | 197 | More cases: TBD 198 | 199 | 200 | ## Proguard 201 | 202 | Proguard configuration isn't very different in standard and multi-feature project configuration. Minification process is enabled in `app/build.gradle` [file](app/build.gradle): 203 | 204 | ```groovy 205 | buildTypes { 206 | debug { 207 | minifyEnabled true 208 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 209 | 'proguard-rules.pro' 210 | testProguardFile 'proguard-test-rules.pro' 211 | } 212 | //... 213 | } 214 | ``` 215 | 216 | For proguard configuration and know-how we could create completely separate demo project and a bunch of articles. Instead, just take a look at screenshots that compare apk files built from this project, with and without minification enabled. 217 | 218 | #### Project without proguard 219 | 220 | ![No proguard config](docs/img/no-proguard.png "No proguard config") 221 | 222 | #### Project with proguard 223 | 224 | ![With proguard config](docs/img/with-proguard.png "With proguard config") 225 | 226 | ### Proguard config for project modules 227 | 228 | It is also possible to Provide proguard configuration for each module separately. Why would you like to do this? Usually Proguard configuration is set in app's module gradle file. Also all global flags `-dontoptimize` also should be set there. 229 | But sometimes there are module-specific configurations. So for example you would like to keep methods or classes, even if they aren't used in app's module. Also when you share .aar library file, you can provide it with Proguard configuration built in. 230 | In this situation you should use `consumerProguardFiles`. For example, see `features/base/build.gradle` (file)[features/base/feature-base-proguard-rules.pro]: 231 | 232 | ```groovy 233 | buildTypes { 234 | all { 235 | consumerProguardFiles 'feature-base-proguard-rules.pro' 236 | } 237 | } 238 | ``` 239 | 240 | Configuration tells: 241 | 242 | ``` 243 | -keep class com.frogermcs.multimodulegithubclient.base.BaseActivity { 244 | public void notUsedMethod(); 245 | } 246 | ``` 247 | 248 | It means that method `notUsedMethod()` from class (BaseActivity)[features/base/src/main/java/com/frogermcs/multimodulegithubclient/base/BaseActivity.java] will be kept, no matter what. 249 | 250 | For more details, take a look at [this blog post](https://proandroiddev.com/handling-proguard-as-library-developer-or-in-a-multi-module-android-application-2d738c37890) that describes how to setup Proguard for multi-module android app. 251 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'jacoco' 3 | 4 | apply from: '../buildsystem/jacoco.gradle' 5 | apply from: '../buildsystem/android_commons.gradle' 6 | 7 | android { 8 | 9 | defaultConfig { 10 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 11 | } 12 | 13 | buildTypes { 14 | debug { 15 | minifyEnabled true 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 17 | 'proguard-rules.pro' 18 | testProguardFile 'proguard-test-rules.pro' 19 | } 20 | 21 | release { 22 | minifyEnabled true 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 24 | 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | packagingOptions { 29 | exclude 'META-INF/androidx.legacy_legacy-support-core-utils.version' 30 | } 31 | 32 | dynamicFeatures = [] 33 | 34 | } 35 | 36 | dependencies { 37 | implementation project(':features:base') 38 | implementation project(':features:repositories') 39 | implementation project(':features:repository') 40 | 41 | rootProject.app.each { item -> 42 | add(item.configuration, item.dependency, item.options) 43 | } 44 | rootProject.annotationProcessorsDependencies.each { item -> 45 | add(item.configuration, item.dependency, item.options) 46 | } 47 | rootProject.unitTestsDependencies.each { item -> 48 | add(item.configuration, item.dependency, item.options) 49 | } 50 | rootProject.instrumentationTestsDependencies.each { item -> 51 | add(item.configuration, item.dependency, item.options) 52 | } 53 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # === General rules === 2 | 3 | -dontobfuscate # Only because of easy debugging 4 | 5 | -dontwarn org.conscrypt.** 6 | -dontwarn javax.lang.model.** 7 | -dontwarn javax.tools.** 8 | -dontwarn java.lang.instrument.** 9 | -dontwarn java.lang.ClassValue 10 | 11 | # === RxJava === 12 | 13 | -dontwarn sun.misc.** 14 | -dontwarn sun.reflect.** 15 | 16 | -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { 17 | long producerIndex; 18 | long consumerIndex; 19 | } 20 | 21 | -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef { 22 | rx.internal.util.atomic.LinkedQueueNode producerNode; 23 | } 24 | 25 | -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef { 26 | rx.internal.util.atomic.LinkedQueueNode consumerNode; 27 | } 28 | 29 | -dontnote rx.internal.util.** 30 | 31 | # ===endof RxJava === 32 | 33 | 34 | # === OkHttp === 35 | 36 | # JSR 305 annotations are for embedding nullability information. 37 | -dontwarn javax.annotation.** 38 | 39 | # A resource is loaded with a relative path so the package of this class must be preserved. 40 | -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase 41 | 42 | # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. 43 | -dontwarn org.codehaus.mojo.animal_sniffer.* 44 | 45 | # OkHttp platform used only on JVM and when Conscrypt dependency is available. 46 | -dontwarn okhttp3.internal.platform.ConscryptPlatform 47 | 48 | -dontnote okhttp3.** 49 | 50 | # ===endof OkHttp === 51 | 52 | 53 | # === Retrofit === 54 | 55 | # Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and 56 | # EnclosingMethod is required to use InnerClasses. 57 | -keepattributes Signature, InnerClasses, EnclosingMethod 58 | 59 | # Retain service method parameters when optimizing. 60 | -keepclassmembers,allowshrinking,allowobfuscation interface * { 61 | @retrofit2.http.* ; 62 | } 63 | 64 | # Ignore annotation used for build tooling. 65 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 66 | 67 | # Ignore JSR 305 annotations for embedding nullability information. 68 | -dontwarn javax.annotation.** 69 | 70 | # Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. 71 | -dontwarn kotlin.Unit 72 | 73 | # Top-level functions that can only be used by Kotlin. 74 | -dontwarn retrofit2.-KotlinExtensions 75 | 76 | # ===endof Retrofit === 77 | 78 | 79 | # === Dagger ==== 80 | 81 | -dontwarn com.google.errorprone.annotations.** 82 | -dontwarn com.squareup.javawriter.** 83 | 84 | # ===endof Dagger === 85 | 86 | 87 | # === Google autofactory === 88 | 89 | -dontwarn com.google.googlejavaformat.** 90 | -dontwarn com.google.common.** 91 | -dontwarn com.google.auto.** 92 | 93 | -dontnote com.google.common.** 94 | -dontnote com.google.auto.** 95 | 96 | # ===endof Google autofactory === 97 | 98 | # === GSON === 99 | 100 | # Gson uses generic type information stored in a class file when working with fields. Proguard 101 | # removes such information by default, so configure it to keep all of it. 102 | -keepattributes Signature 103 | 104 | # For using GSON @Expose annotation 105 | -keepattributes *Annotation* 106 | 107 | # Gson specific classes 108 | -dontwarn sun.misc.** 109 | #-keep class com.google.gson.stream.** { *; } 110 | 111 | # Application classes that will be serialized/deserialized over Gson 112 | -keep class com.google.gson.examples.android.model.** { *; } 113 | 114 | # Prevent proguard from stripping interface information from TypeAdapterFactory, 115 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) 116 | -keep class * implements com.google.gson.TypeAdapterFactory 117 | -keep class * implements com.google.gson.JsonSerializer 118 | -keep class * implements com.google.gson.JsonDeserializer 119 | 120 | -dontnote com.google.gson.internal.** 121 | 122 | # ===endof GSON === 123 | 124 | 125 | 126 | # === App UI === 127 | 128 | -keep class androidx.recyclerview.widget.RecyclerView { 129 | public ; 130 | } 131 | -keep class androidx.coordinatorlayout.widget.CoordinatorLayout$Behavior {*;} 132 | 133 | # ===endof App UI === -------------------------------------------------------------------------------- /app/proguard-test-rules.pro: -------------------------------------------------------------------------------- 1 | -dontwarn org.xmlpull.v1.** 2 | 3 | -dontwarn androidx.test.espresso.** 4 | 5 | -dontwarn android.support.test.services.** 6 | -dontwarn android.support.test.runner.** 7 | -dontwarn android.support.test.orchestrator.** 8 | 9 | -dontnote androidx.transition.** 10 | -dontnote androidx.test.runner.** 11 | -dontnote androidx.test.espresso.** 12 | -dontnote android.support.test.** 13 | -dontnote org.junit.** 14 | -dontnote junit.framework.** 15 | -dontnote junit.runner.** 16 | -dontnote org.xmlpull.** 17 | -dontnote java.lang.invoke.** 18 | -dontnote org.apache.** 19 | -dontnote android.net.** 20 | 21 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/frogermcs/multimodulegithubclient/endtoend/AppFlowTest.java: -------------------------------------------------------------------------------- 1 | package com.frogermcs.multimodulegithubclient.endtoend; 2 | 3 | import android.view.View; 4 | import android.widget.TextView; 5 | 6 | import com.frogermcs.multimodulegithubclient.SplashActivity; 7 | import com.frogermcs.multimodulegithubclient.repositories.RepositoriesListActivity; 8 | 9 | import org.hamcrest.Matcher; 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | 14 | import androidx.appcompat.widget.Toolbar; 15 | import androidx.recyclerview.widget.RecyclerView; 16 | import androidx.test.espresso.UiController; 17 | import androidx.test.espresso.ViewAction; 18 | import androidx.test.espresso.contrib.RecyclerViewActions; 19 | import androidx.test.espresso.intent.rule.IntentsTestRule; 20 | import androidx.test.filters.LargeTest; 21 | import androidx.test.runner.AndroidJUnit4; 22 | 23 | import static androidx.test.espresso.Espresso.onView; 24 | import static androidx.test.espresso.action.ViewActions.click; 25 | import static androidx.test.espresso.action.ViewActions.typeText; 26 | import static androidx.test.espresso.assertion.ViewAssertions.matches; 27 | import static androidx.test.espresso.intent.Intents.intended; 28 | import static androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent; 29 | import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom; 30 | import static androidx.test.espresso.matcher.ViewMatchers.isRoot; 31 | import static androidx.test.espresso.matcher.ViewMatchers.withHint; 32 | import static androidx.test.espresso.matcher.ViewMatchers.withParent; 33 | import static androidx.test.espresso.matcher.ViewMatchers.withText; 34 | import static org.hamcrest.CoreMatchers.allOf; 35 | import static org.hamcrest.CoreMatchers.instanceOf; 36 | 37 | @RunWith(AndroidJUnit4.class) 38 | @LargeTest 39 | public class AppFlowTest { 40 | @Rule 41 | public IntentsTestRule splashActivityRule = new IntentsTestRule<>(SplashActivity.class); 42 | 43 | @Test 44 | public void goThroughAllScreens_HappyPath() { 45 | 46 | // 47 | // ===== Main Screen ===== 48 | // 49 | onView(withHint("username")).perform(typeText("frogermcs")); 50 | onView(withText("Show repositories")).perform(click()); 51 | 52 | // Dirty 'Tread.sleep()' replacement. The solution can be way more elegant here. :) 53 | onView(isRoot()).perform(waitFor(2000)); 54 | 55 | // 56 | // ===== Repositories list screen ===== 57 | // 58 | 59 | // Is Activity started ? 60 | //Don't use if this is end-to-end test. In theory you shouldn't have knowledge about classes and implementation 61 | intended(hasComponent(RepositoriesListActivity.class.getName())); 62 | 63 | //Assert screen title 64 | onView( 65 | allOf( 66 | isAssignableFrom(TextView.class), 67 | withParent(isAssignableFrom(Toolbar.class)) 68 | )) 69 | .check(matches(withText("Repositories list"))); 70 | 71 | onView(instanceOf(RecyclerView.class)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click())); 72 | 73 | onView(isRoot()).perform(waitFor(500)); 74 | 75 | // 76 | // ===== Repository details ===== 77 | // 78 | 79 | // Is Activity started? This time Activity isn't a direct part of app module, 80 | // so class cannot be pointed directly. 81 | //Plus again, you shouldn't use this in end-to-end tests... 82 | intended(hasComponent("com.frogermcs.multimodulegithubclient.repository.RepositoryDetailsActivity")); 83 | 84 | //Assert screen title 85 | onView( 86 | allOf( 87 | isAssignableFrom(TextView.class), 88 | withParent(isAssignableFrom(Toolbar.class)) 89 | )) 90 | .check(matches(withText("Repository details"))); 91 | } 92 | 93 | public static ViewAction waitFor(final long millis) { 94 | return new ViewAction() { 95 | @Override 96 | public Matcher getConstraints() { 97 | return isRoot(); 98 | } 99 | 100 | @Override 101 | public String getDescription() { 102 | return "Wait for " + millis + " milliseconds."; 103 | } 104 | 105 | @Override 106 | public void perform(UiController uiController, final View view) { 107 | uiController.loopMainThreadForAtLeast(millis); 108 | } 109 | }; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/frogermcs/multimodulegithubclient/App.java: -------------------------------------------------------------------------------- 1 | package com.frogermcs.multimodulegithubclient; 2 | 3 | import android.app.Application; 4 | 5 | import com.frogermcs.multimodulegithubclient.base.BuildConfig; 6 | 7 | import timber.log.Timber; 8 | 9 | /** 10 | * Created by Miroslaw Stanek on 22.04.15. 11 | */ 12 | public class App extends Application { 13 | 14 | @Override 15 | public void onCreate() { 16 | super.onCreate(); 17 | if (BuildConfig.DEBUG) { 18 | Timber.plant(new Timber.DebugTree()); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/frogermcs/multimodulegithubclient/AppComponent.java: -------------------------------------------------------------------------------- 1 | package com.frogermcs.multimodulegithubclient; 2 | 3 | import com.frogermcs.multimodulegithubclient.base.BaseComponent; 4 | 5 | import dagger.Component; 6 | 7 | @AppScope 8 | @Component( 9 | modules = GithubClientModule.class, 10 | dependencies = BaseComponent.class 11 | ) 12 | public interface AppComponent { 13 | SplashActivityComponent plus(SplashActivityModule module); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/frogermcs/multimodulegithubclient/AppComponentWrapper.java: -------------------------------------------------------------------------------- 1 | package com.frogermcs.multimodulegithubclient; 2 | 3 | import android.app.Application; 4 | 5 | import com.frogermcs.multimodulegithubclient.base.BaseComponent; 6 | import com.frogermcs.multimodulegithubclient.base.BaseComponentWrapper; 7 | 8 | public class AppComponentWrapper { 9 | 10 | private static AppComponentWrapper appComponentWrapper; 11 | 12 | private AppComponentWrapper() { 13 | 14 | } 15 | 16 | public static AppComponentWrapper getInstance(Application application) { 17 | if (appComponentWrapper == null) { 18 | synchronized (AppComponentWrapper.class) { 19 | if (appComponentWrapper == null) { 20 | appComponentWrapper = new AppComponentWrapper(); 21 | appComponentWrapper.initializeComponent(BaseComponentWrapper.getBaseComponent(application)); 22 | } 23 | } 24 | } 25 | return appComponentWrapper; 26 | } 27 | 28 | private AppComponent appComponent; 29 | 30 | public static AppComponent getAppComponent(Application application) { 31 | AppComponentWrapper appComponentWrapper = getInstance(application); 32 | return appComponentWrapper.appComponent; 33 | } 34 | 35 | public AppComponent initializeComponent(BaseComponent baseComponent) { 36 | appComponent = DaggerAppComponent.builder() 37 | .baseComponent(baseComponent) 38 | .build(); 39 | return appComponent; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/frogermcs/multimodulegithubclient/AppScope.java: -------------------------------------------------------------------------------- 1 | package com.frogermcs.multimodulegithubclient; 2 | 3 | import javax.inject.Scope; 4 | 5 | @Scope 6 | public @interface AppScope { 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/frogermcs/multimodulegithubclient/GithubClientModule.java: -------------------------------------------------------------------------------- 1 | package com.frogermcs.multimodulegithubclient; 2 | 3 | import com.frogermcs.multimodulegithubclient.base.data.api.GithubApiService; 4 | import com.frogermcs.multimodulegithubclient.base.data.api.UserManager; 5 | 6 | import dagger.Module; 7 | import dagger.Provides; 8 | 9 | @Module 10 | public class GithubClientModule { 11 | 12 | @Provides 13 | @AppScope 14 | public UserManager provideUserManager(GithubApiService githubApiService) { 15 | return new UserManager(githubApiService); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/frogermcs/multimodulegithubclient/SplashActivity.java: -------------------------------------------------------------------------------- 1 | package com.frogermcs.multimodulegithubclient; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | import android.widget.Button; 6 | import android.widget.EditText; 7 | import android.widget.ProgressBar; 8 | 9 | import com.frogermcs.multimodulegithubclient.base.BaseActivity; 10 | import com.frogermcs.multimodulegithubclient.base.data.model.User; 11 | import com.frogermcs.multimodulegithubclient.base.utils.AnalyticsManager; 12 | import com.frogermcs.multimodulegithubclient.repositories.RepositoriesListActivity; 13 | import com.jakewharton.rxbinding.widget.RxTextView; 14 | 15 | import javax.inject.Inject; 16 | 17 | import butterknife.BindView; 18 | import butterknife.OnClick; 19 | import rx.Subscription; 20 | 21 | 22 | public class SplashActivity extends BaseActivity { 23 | 24 | @BindView(R.id.etUsername) 25 | EditText etUsername; 26 | @BindView(R.id.pbLoading) 27 | ProgressBar pbLoading; 28 | @BindView(R.id.btnShowRepositories) 29 | Button btnShowRepositories; 30 | 31 | @Inject 32 | SplashActivityPresenter presenter; 33 | @Inject 34 | AnalyticsManager analyticsManager; 35 | 36 | private Subscription textChangeSubscription; 37 | 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_splash); 42 | textChangeSubscription = RxTextView.textChangeEvents(etUsername) 43 | .subscribe(textViewTextChangeEvent -> { 44 | presenter.username = textViewTextChangeEvent.text().toString(); 45 | etUsername.setError(null); 46 | }); 47 | presenter.init(); 48 | } 49 | 50 | @Override 51 | protected void onDestroy() { 52 | super.onDestroy(); 53 | textChangeSubscription.unsubscribe(); 54 | } 55 | 56 | @Override 57 | protected void setupActivityComponent() { 58 | AppComponentWrapper 59 | .getAppComponent(getApplication()) 60 | .plus(new SplashActivityModule(this)) 61 | .inject(this); 62 | } 63 | 64 | @Override 65 | public String getScreenName() { 66 | return "Splash"; 67 | } 68 | 69 | @OnClick(R.id.btnShowRepositories) 70 | public void onShowRepositoriesClick() { 71 | presenter.onShowRepositoriesClick(); 72 | } 73 | 74 | public void showRepositoriesListForUser(User user) { 75 | RepositoriesListActivity.startRepositoriesListActivity(user.login, this); 76 | } 77 | 78 | public void showValidationError() { 79 | etUsername.setError("Validation error"); 80 | } 81 | 82 | public void showLoading(boolean loading) { 83 | btnShowRepositories.setVisibility(loading ? View.GONE : View.VISIBLE); 84 | pbLoading.setVisibility(loading ? View.VISIBLE : View.GONE); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/frogermcs/multimodulegithubclient/SplashActivityComponent.java: -------------------------------------------------------------------------------- 1 | package com.frogermcs.multimodulegithubclient; 2 | 3 | import com.frogermcs.multimodulegithubclient.base.ActivityScope; 4 | 5 | import dagger.Subcomponent; 6 | 7 | /** 8 | * Created by Miroslaw Stanek on 23.04.15. 9 | */ 10 | @ActivityScope 11 | @Subcomponent( 12 | modules = SplashActivityModule.class 13 | ) 14 | public interface SplashActivityComponent { 15 | 16 | SplashActivity inject(SplashActivity splashActivity); 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/frogermcs/multimodulegithubclient/SplashActivityModule.java: -------------------------------------------------------------------------------- 1 | package com.frogermcs.multimodulegithubclient; 2 | 3 | import com.frogermcs.multimodulegithubclient.base.ActivityScope; 4 | import com.frogermcs.multimodulegithubclient.base.data.api.UserManager; 5 | import com.frogermcs.multimodulegithubclient.base.utils.AnalyticsManager; 6 | import com.frogermcs.multimodulegithubclient.base.utils.Validator; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | 11 | /** 12 | * Created by Miroslaw Stanek on 23.04.15. 13 | */ 14 | @Module 15 | public class SplashActivityModule { 16 | private SplashActivity splashActivity; 17 | 18 | public SplashActivityModule(SplashActivity splashActivity) { 19 | this.splashActivity = splashActivity; 20 | } 21 | 22 | @Provides 23 | @ActivityScope 24 | SplashActivity provideSplashActivity() { 25 | return splashActivity; 26 | } 27 | 28 | @Provides 29 | @ActivityScope 30 | SplashActivityPresenter 31 | provideSplashActivityPresenter(Validator validator, 32 | UserManager userManager, 33 | AnalyticsManager analyticsManager) { 34 | return new SplashActivityPresenter( 35 | splashActivity, 36 | validator, 37 | userManager, 38 | analyticsManager 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/frogermcs/multimodulegithubclient/SplashActivityPresenter.java: -------------------------------------------------------------------------------- 1 | package com.frogermcs.multimodulegithubclient; 2 | 3 | import com.frogermcs.multimodulegithubclient.base.data.api.UserManager; 4 | import com.frogermcs.multimodulegithubclient.base.utils.AnalyticsManager; 5 | import com.frogermcs.multimodulegithubclient.base.utils.Validator; 6 | 7 | /** 8 | * Created by Miroslaw Stanek on 23.04.15. 9 | */ 10 | public class SplashActivityPresenter { 11 | public String username; 12 | 13 | private final SplashActivity splashActivity; 14 | private final Validator validator; 15 | private final UserManager userManager; 16 | private final AnalyticsManager analyticsManager; 17 | 18 | public SplashActivityPresenter(SplashActivity splashActivity, 19 | Validator validator, 20 | UserManager userManager, 21 | AnalyticsManager analyticsManager) { 22 | this.splashActivity = splashActivity; 23 | this.validator = validator; 24 | this.userManager = userManager; 25 | this.analyticsManager = analyticsManager; 26 | } 27 | 28 | public void init() { 29 | analyticsManager.logScreenView(splashActivity.getScreenName()); 30 | } 31 | 32 | public void onShowRepositoriesClick() { 33 | if (validator.validUsername(username)) { 34 | splashActivity.showLoading(true); 35 | userManager.getUser(username) 36 | .doOnTerminate(() -> splashActivity.showLoading(false)) 37 | .subscribe( 38 | splashActivity::showRepositoriesListForUser, 39 | throwable -> splashActivity.showValidationError()); 40 | } else { 41 | splashActivity.showValidationError(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 20 | 21 |