Repository: android10/Android-CleanArchitecture Branch: master Commit: 8ed4222c537e Files: 116 Total size: 214.4 KB Directory structure: gitextract_60j9_cvf/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── buildsystem/ │ ├── ci.gradle │ ├── debug.keystore │ └── dependencies.gradle ├── data/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── fernandocejas/ │ │ └── android10/ │ │ └── sample/ │ │ └── data/ │ │ ├── cache/ │ │ │ ├── FileManager.java │ │ │ ├── UserCache.java │ │ │ ├── UserCacheImpl.java │ │ │ └── serializer/ │ │ │ └── Serializer.java │ │ ├── entity/ │ │ │ ├── UserEntity.java │ │ │ └── mapper/ │ │ │ ├── UserEntityDataMapper.java │ │ │ └── UserEntityJsonMapper.java │ │ ├── exception/ │ │ │ ├── NetworkConnectionException.java │ │ │ ├── RepositoryErrorBundle.java │ │ │ └── UserNotFoundException.java │ │ ├── executor/ │ │ │ └── JobExecutor.java │ │ ├── net/ │ │ │ ├── ApiConnection.java │ │ │ ├── RestApi.java │ │ │ └── RestApiImpl.java │ │ └── repository/ │ │ ├── UserDataRepository.java │ │ └── datasource/ │ │ ├── CloudUserDataStore.java │ │ ├── DiskUserDataStore.java │ │ ├── UserDataStore.java │ │ └── UserDataStoreFactory.java │ └── test/ │ └── java/ │ └── com/ │ └── fernandocejas/ │ └── android10/ │ └── sample/ │ └── data/ │ ├── ApplicationStub.java │ ├── ApplicationTestCase.java │ ├── cache/ │ │ ├── FileManagerTest.java │ │ └── serializer/ │ │ └── SerializerTest.java │ ├── entity/ │ │ └── mapper/ │ │ ├── UserEntityDataMapperTest.java │ │ └── UserEntityJsonMapperTest.java │ ├── exception/ │ │ └── RepositoryErrorBundleTest.java │ └── repository/ │ ├── UserDataRepositoryTest.java │ └── datasource/ │ ├── CloudUserDataStoreTest.java │ ├── DiskUserDataStoreTest.java │ └── UserDataStoreFactoryTest.java ├── domain/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── fernandocejas/ │ │ └── android10/ │ │ └── sample/ │ │ └── domain/ │ │ ├── User.java │ │ ├── exception/ │ │ │ ├── DefaultErrorBundle.java │ │ │ └── ErrorBundle.java │ │ ├── executor/ │ │ │ ├── PostExecutionThread.java │ │ │ └── ThreadExecutor.java │ │ ├── interactor/ │ │ │ ├── DefaultObserver.java │ │ │ ├── GetUserDetails.java │ │ │ ├── GetUserList.java │ │ │ └── UseCase.java │ │ └── repository/ │ │ └── UserRepository.java │ └── test/ │ └── java/ │ └── com/ │ └── fernandocejas/ │ └── android10/ │ └── sample/ │ └── domain/ │ ├── UserTest.java │ ├── exception/ │ │ └── DefaultErrorBundleTest.java │ └── interactor/ │ ├── GetUserDetailsTest.java │ ├── GetUserListTest.java │ └── UseCaseTest.java ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── presentation/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── fernandocejas/ │ │ └── android10/ │ │ └── sample/ │ │ └── test/ │ │ ├── exception/ │ │ │ └── ErrorMessageFactoryTest.java │ │ ├── mapper/ │ │ │ └── UserModelDataMapperTest.java │ │ ├── presenter/ │ │ │ ├── UserDetailsPresenterTest.java │ │ │ └── UserListPresenterTest.java │ │ └── view/ │ │ └── activity/ │ │ ├── UserDetailsActivityTest.java │ │ └── UserListActivityTest.java │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── fernandocejas/ │ │ └── android10/ │ │ └── sample/ │ │ └── presentation/ │ │ ├── AndroidApplication.java │ │ ├── UIThread.java │ │ ├── exception/ │ │ │ └── ErrorMessageFactory.java │ │ ├── internal/ │ │ │ └── di/ │ │ │ ├── HasComponent.java │ │ │ ├── PerActivity.java │ │ │ ├── components/ │ │ │ │ ├── ActivityComponent.java │ │ │ │ ├── ApplicationComponent.java │ │ │ │ └── UserComponent.java │ │ │ └── modules/ │ │ │ ├── ActivityModule.java │ │ │ ├── ApplicationModule.java │ │ │ └── UserModule.java │ │ ├── mapper/ │ │ │ └── UserModelDataMapper.java │ │ ├── model/ │ │ │ └── UserModel.java │ │ ├── navigation/ │ │ │ └── Navigator.java │ │ ├── presenter/ │ │ │ ├── Presenter.java │ │ │ ├── UserDetailsPresenter.java │ │ │ └── UserListPresenter.java │ │ └── view/ │ │ ├── LoadDataView.java │ │ ├── UserDetailsView.java │ │ ├── UserListView.java │ │ ├── activity/ │ │ │ ├── BaseActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── UserDetailsActivity.java │ │ │ └── UserListActivity.java │ │ ├── adapter/ │ │ │ ├── UsersAdapter.java │ │ │ └── UsersLayoutManager.java │ │ ├── component/ │ │ │ └── AutoLoadImageView.java │ │ └── fragment/ │ │ ├── BaseFragment.java │ │ ├── UserDetailsFragment.java │ │ └── UserListFragment.java │ └── res/ │ ├── drawable/ │ │ └── selector_item_user.xml │ ├── layout/ │ │ ├── activity_layout.xml │ │ ├── activity_main.xml │ │ ├── fragment_user_details.xml │ │ ├── fragment_user_list.xml │ │ ├── row_user.xml │ │ ├── view_progress.xml │ │ ├── view_retry.xml │ │ └── view_user_details.xml │ └── values/ │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Windows thumbnail db Thumbs.db # OSX files .DS_Store # built application files *.apk *.ap_ # files for the dex VM *.dex # Java class files *.class # generated files bin/ gen/ build/ # Local configuration file (sdk path, etc) local.properties # Eclipse project files .classpath .project # Android Studio .idea .gradle /*/local.properties /*/out /*/*/build build /*/*/production *.iml *.iws *.ipr *~ *.swp ================================================ FILE: .travis.yml ================================================ language: android jdk: oraclejdk8 android: components: - tools - platform-tools - tools - build-tools-27.0.1 - android-26 - extra-google-m2repository - extra-android-m2repository licenses: - 'android-sdk-preview-license-.+' - 'android-sdk-license-.+' - 'google-gdk-license-.+' script: ./gradlew build ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ Android-CleanArchitecture ========================= ## New version available written in Kotlin: [Architecting Android… Reloaded](https://fernandocejas.com/2018/05/07/architecting-android-reloaded/) Introduction ----------------- This is a sample app that is part of a blog post I have written about how to architect android application using the Uncle Bob's clean architecture approach. [Architecting Android…The clean way?](http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/) [Architecting Android…The evolution](http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/) [Tasting Dagger 2 on Android](http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android/) [Clean Architecture…Dynamic Parameters in Use Cases](http://fernandocejas.com/2016/12/24/clean-architecture-dynamic-parameters-in-use-cases/) [Demo video of this sample](http://youtu.be/XSjV4sG3ni0) Clean architecture ----------------- ![http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/](https://github.com/android10/Sample-Data/blob/master/Android-CleanArchitecture/clean_architecture.png) Architectural approach ----------------- ![http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/](https://github.com/android10/Sample-Data/blob/master/Android-CleanArchitecture/clean_architecture_layers.png) Architectural reactive approach ----------------- ![http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/](https://github.com/android10/Sample-Data/blob/master/Android-CleanArchitecture/clean_architecture_layers_details.png) Local Development ----------------- Here are some useful Gradle/adb commands for executing this example: * `./gradlew clean build` - Build the entire example and execute unit and integration tests plus lint check. * `./gradlew installDebug` - Install the debug apk on the current connected device. * `./gradlew runUnitTests` - Execute domain and data layer tests (both unit and integration). * `./gradlew runAcceptanceTests` - Execute espresso and instrumentation acceptance tests. Discussions ----------------- Refer to the issues section: https://github.com/android10/Android-CleanArchitecture/issues Code style ----------- Here you can download and install the java codestyle. https://github.com/android10/java-code-styles License -------- Copyright 2018 Fernando Cejas Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ![http://www.fernandocejas.com](https://github.com/android10/Sample-Data/blob/master/android10/android10_logo_big.png) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Android--CleanArchitecture-brightgreen.svg?style=flat)](https://android-arsenal.com/details/3/909) Buy Me A Coffee ================================================ FILE: build.gradle ================================================ apply from: 'buildsystem/ci.gradle' apply from: 'buildsystem/dependencies.gradle' buildscript { repositories { jcenter() mavenCentral() google() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' // classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' } } allprojects { ext { androidApplicationId = 'com.fernanependocejas.android10.sample.presentation' androidVersionCode = 1 androidVersionName = "1.0" testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" testApplicationId = 'com.fernandocejas.android10.sample.presentation.test' } } task runDomainUnitTests(dependsOn: [':domain:test']) { description 'Run unit tests for the domain layer.' } task runDataUnitTests(dependsOn: [':data:cleanTestDebugUnitTest', ':data:testDebugUnitTest']) { description 'Run unit tests for the data layer.' } task runUnitTests(dependsOn: ['runDomainUnitTests', 'runDataUnitTests']) { description 'Run unit tests for both domain and data layers.' } task runAcceptanceTests(dependsOn: [':presentation:connectedAndroidTest']) { description 'Run application acceptance tests.' } ================================================ FILE: buildsystem/ci.gradle ================================================ def ciServer = 'TRAVIS' def executingOnCI = "true".equals(System.getenv(ciServer)) // Since for CI we always do full clean builds, we don't want to pre-dex // See http://tools.android.com/tech-docs/new-build-system/tips subprojects { project.plugins.whenPluginAdded { plugin -> if ('com.android.build.gradle.AppPlugin'.equals(plugin.class.name) || 'com.android.build.gradle.LibraryPlugin'.equals(plugin.class.name)) { project.android.dexOptions.preDexLibraries = !executingOnCI } } } ================================================ FILE: buildsystem/dependencies.gradle ================================================ allprojects { repositories { jcenter() } } ext { //Android androidBuildToolsVersion = "27.0.1" androidMinSdkVersion = 15 androidTargetSdkVersion = 26 androidCompileSdkVersion = 26 //Libraries daggerVersion = '2.8' butterKnifeVersion = '7.0.1' recyclerViewVersion = '25.4.0' rxJavaVersion = '2.0.2' rxAndroidVersion = '2.0.1' javaxAnnotationVersion = '1.0' javaxInjectVersion = '1' gsonVersion = '2.3' okHttpVersion = '2.5.0' androidAnnotationsVersion = '25.4.0' arrowVersion = '1.0.0' //Testing robolectricVersion = '3.1.1' jUnitVersion = '4.12' assertJVersion = '1.7.1' mockitoVersion = '1.9.5' dexmakerVersion = '1.0' espressoVersion = '3.0.1' testingSupportLibVersion = '0.1' //Development leakCanaryVersion = '1.3.1' presentationDependencies = [ daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", dagger: "com.google.dagger:dagger:${daggerVersion}", butterKnife: "com.jakewharton:butterknife:${butterKnifeVersion}", recyclerView: "com.android.support:recyclerview-v7:${recyclerViewVersion}", rxJava: "io.reactivex.rxjava2:rxjava:${rxJavaVersion}", rxAndroid: "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}" ] presentationTestDependencies = [ mockito: "org.mockito:mockito-core:${mockitoVersion}", dexmaker: "com.google.dexmaker:dexmaker:${dexmakerVersion}", dexmakerMockito: "com.google.dexmaker:dexmaker-mockito:${dexmakerVersion}", espresso: "com.android.support.test.espresso:espresso-core:${espressoVersion}", testingSupportLib: "com.android.support.test:testing-support-lib:${testingSupportLibVersion}", ] domainDependencies = [ javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", rxJava: "io.reactivex.rxjava2:rxjava:${rxJavaVersion}", arrow: "com.fernandocejas:arrow:${arrowVersion}" ] domainTestDependencies = [ junit: "junit:junit:${jUnitVersion}", mockito: "org.mockito:mockito-core:${mockitoVersion}", assertj: "org.assertj:assertj-core:${assertJVersion}" ] dataDependencies = [ daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", dagger: "com.google.dagger:dagger:${daggerVersion}", okHttp: "com.squareup.okhttp:okhttp:${okHttpVersion}", gson: "com.google.code.gson:gson:${gsonVersion}", rxJava: "io.reactivex.rxjava2:rxjava:${rxJavaVersion}", rxAndroid: "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}" ] dataTestDependencies = [ junit: "junit:junit:${jUnitVersion}", assertj: "org.assertj:assertj-core:${assertJVersion}", mockito: "org.mockito:mockito-core:${mockitoVersion}", robolectric: "org.robolectric:robolectric:${robolectricVersion}", ] developmentDependencies = [ leakCanary: "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}", ] } ================================================ FILE: data/.gitignore ================================================ /build ================================================ FILE: data/build.gradle ================================================ buildscript { repositories { mavenCentral() } dependencies { classpath 'me.tatarka:gradle-retrolambda:3.7.0' } } apply plugin: 'com.android.library' //apply plugin: 'com.neenbedankt.android-apt' apply plugin: 'me.tatarka.retrolambda' android { defaultPublishConfig "debug" def globalConfiguration = rootProject.extensions.getByName("ext") compileSdkVersion globalConfiguration.getAt("androidCompileSdkVersion") buildToolsVersion globalConfiguration.getAt("androidBuildToolsVersion") defaultConfig { minSdkVersion globalConfiguration.getAt("androidMinSdkVersion") targetSdkVersion globalConfiguration.getAt("androidTargetSdkVersion") versionCode globalConfiguration.getAt("androidVersionCode") } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } packagingOptions { exclude 'LICENSE.txt' exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/ASL2.0' exclude 'META-INF/NOTICE' exclude 'META-INF/LICENSE' } lintOptions { quiet true abortOnError false ignoreWarnings true disable 'InvalidPackage' // Some libraries have issues with this disable 'OldTargetApi' // Due to Robolectric that modifies the manifest when running tests } } dependencies { def dataDependencies = rootProject.ext.dataDependencies def testDependencies = rootProject.ext.dataTestDependencies implementation project(':domain') compileOnly dataDependencies.javaxAnnotation implementation dataDependencies.javaxInject implementation dataDependencies.okHttp implementation dataDependencies.gson implementation dataDependencies.rxJava implementation dataDependencies.rxAndroid implementation dataDependencies.androidAnnotations testImplementation testDependencies.junit testImplementation testDependencies.assertj testImplementation testDependencies.mockito testImplementation testDependencies.robolectric } repositories { google() } ================================================ FILE: data/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/fcejas/Software/SDKs/android-sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: data/src/main/AndroidManifest.xml ================================================ ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/cache/FileManager.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.cache; import android.content.Context; import android.content.SharedPreferences; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import javax.inject.Inject; import javax.inject.Singleton; /** * Helper class to do operations on regular files/directories. */ @Singleton public class FileManager { @Inject FileManager() {} /** * Writes a file to Disk. * This is an I/O operation and this method executes in the main thread, so it is recommended to * perform this operation using another thread. * * @param file The file to write to Disk. */ void writeToFile(File file, String fileContent) { if (!file.exists()) { try { final FileWriter writer = new FileWriter(file); writer.write(fileContent); writer.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * Reads a content from a file. * This is an I/O operation and this method executes in the main thread, so it is recommended to * perform the operation using another thread. * * @param file The file to read from. * @return A string with the content of the file. */ String readFileContent(File file) { final StringBuilder fileContentBuilder = new StringBuilder(); if (file.exists()) { String stringLine; try { final FileReader fileReader = new FileReader(file); final BufferedReader bufferedReader = new BufferedReader(fileReader); while ((stringLine = bufferedReader.readLine()) != null) { fileContentBuilder.append(stringLine).append("\n"); } bufferedReader.close(); fileReader.close(); } catch (IOException e) { e.printStackTrace(); } } return fileContentBuilder.toString(); } /** * Returns a boolean indicating whether this file can be found on the underlying file system. * * @param file The file to check existence. * @return true if this file exists, false otherwise. */ boolean exists(File file) { return file.exists(); } /** * Warning: Deletes the content of a directory. * This is an I/O operation and this method executes in the main thread, so it is recommended to * perform the operation using another thread. * * @param directory The directory which its content will be deleted. */ boolean clearDirectory(File directory) { boolean result = false; if (directory.exists()) { for (File file : directory.listFiles()) { result = file.delete(); } } return result; } /** * Write a value to a user preferences file. * * @param context {@link android.content.Context} to retrieve android user preferences. * @param preferenceFileName A file name reprensenting where data will be written to. * @param key A string for the key that will be used to retrieve the value in the future. * @param value A long representing the value to be inserted. */ void writeToPreferences(Context context, String preferenceFileName, String key, long value) { final SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceFileName, Context.MODE_PRIVATE); final SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putLong(key, value); editor.apply(); } /** * Get a value from a user preferences file. * * @param context {@link android.content.Context} to retrieve android user preferences. * @param preferenceFileName A file name representing where data will be get from. * @param key A key that will be used to retrieve the value from the preference file. * @return A long representing the value retrieved from the preferences file. */ long getFromPreferences(Context context, String preferenceFileName, String key) { final SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceFileName, Context.MODE_PRIVATE); return sharedPreferences.getLong(key, 0); } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/cache/UserCache.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.cache; import com.fernandocejas.android10.sample.data.entity.UserEntity; import io.reactivex.Observable; /** * An interface representing a user Cache. */ public interface UserCache { /** * Gets an {@link Observable} which will emit a {@link UserEntity}. * * @param userId The user id to retrieve data. */ Observable get(final int userId); /** * Puts and element into the cache. * * @param userEntity Element to insert in the cache. */ void put(UserEntity userEntity); /** * Checks if an element (User) exists in the cache. * * @param userId The id used to look for inside the cache. * @return true if the element is cached, otherwise false. */ boolean isCached(final int userId); /** * Checks if the cache is expired. * * @return true, the cache is expired, otherwise false. */ boolean isExpired(); /** * Evict all elements of the cache. */ void evictAll(); } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/cache/UserCacheImpl.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.cache; import android.content.Context; import com.fernandocejas.android10.sample.data.cache.serializer.Serializer; import com.fernandocejas.android10.sample.data.entity.UserEntity; import com.fernandocejas.android10.sample.data.exception.UserNotFoundException; import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; import io.reactivex.Observable; import java.io.File; import javax.inject.Inject; import javax.inject.Singleton; /** * {@link UserCache} implementation. */ @Singleton public class UserCacheImpl implements UserCache { private static final String SETTINGS_FILE_NAME = "com.fernandocejas.android10.SETTINGS"; private static final String SETTINGS_KEY_LAST_CACHE_UPDATE = "last_cache_update"; private static final String DEFAULT_FILE_NAME = "user_"; private static final long EXPIRATION_TIME = 60 * 10 * 1000; private final Context context; private final File cacheDir; private final Serializer serializer; private final FileManager fileManager; private final ThreadExecutor threadExecutor; /** * Constructor of the class {@link UserCacheImpl}. * * @param context A * @param serializer {@link Serializer} for object serialization. * @param fileManager {@link FileManager} for saving serialized objects to the file system. */ @Inject UserCacheImpl(Context context, Serializer serializer, FileManager fileManager, ThreadExecutor executor) { if (context == null || serializer == null || fileManager == null || executor == null) { throw new IllegalArgumentException("Invalid null parameter"); } this.context = context.getApplicationContext(); this.cacheDir = this.context.getCacheDir(); this.serializer = serializer; this.fileManager = fileManager; this.threadExecutor = executor; } @Override public Observable get(final int userId) { return Observable.create(emitter -> { final File userEntityFile = UserCacheImpl.this.buildFile(userId); final String fileContent = UserCacheImpl.this.fileManager.readFileContent(userEntityFile); final UserEntity userEntity = UserCacheImpl.this.serializer.deserialize(fileContent, UserEntity.class); if (userEntity != null) { emitter.onNext(userEntity); emitter.onComplete(); } else { emitter.onError(new UserNotFoundException()); } }); } @Override public void put(UserEntity userEntity) { if (userEntity != null) { final File userEntityFile = this.buildFile(userEntity.getUserId()); if (!isCached(userEntity.getUserId())) { final String jsonString = this.serializer.serialize(userEntity, UserEntity.class); this.executeAsynchronously(new CacheWriter(this.fileManager, userEntityFile, jsonString)); setLastCacheUpdateTimeMillis(); } } } @Override public boolean isCached(int userId) { final File userEntityFile = this.buildFile(userId); return this.fileManager.exists(userEntityFile); } @Override public boolean isExpired() { long currentTime = System.currentTimeMillis(); long lastUpdateTime = this.getLastCacheUpdateTimeMillis(); boolean expired = ((currentTime - lastUpdateTime) > EXPIRATION_TIME); if (expired) { this.evictAll(); } return expired; } @Override public void evictAll() { this.executeAsynchronously(new CacheEvictor(this.fileManager, this.cacheDir)); } /** * Build a file, used to be inserted in the disk cache. * * @param userId The id user to build the file. * @return A valid file. */ private File buildFile(int userId) { final StringBuilder fileNameBuilder = new StringBuilder(); fileNameBuilder.append(this.cacheDir.getPath()); fileNameBuilder.append(File.separator); fileNameBuilder.append(DEFAULT_FILE_NAME); fileNameBuilder.append(userId); return new File(fileNameBuilder.toString()); } /** * Set in millis, the last time the cache was accessed. */ private void setLastCacheUpdateTimeMillis() { final long currentMillis = System.currentTimeMillis(); this.fileManager.writeToPreferences(this.context, SETTINGS_FILE_NAME, SETTINGS_KEY_LAST_CACHE_UPDATE, currentMillis); } /** * Get in millis, the last time the cache was accessed. */ private long getLastCacheUpdateTimeMillis() { return this.fileManager.getFromPreferences(this.context, SETTINGS_FILE_NAME, SETTINGS_KEY_LAST_CACHE_UPDATE); } /** * Executes a {@link Runnable} in another Thread. * * @param runnable {@link Runnable} to execute */ private void executeAsynchronously(Runnable runnable) { this.threadExecutor.execute(runnable); } /** * {@link Runnable} class for writing to disk. */ private static class CacheWriter implements Runnable { private final FileManager fileManager; private final File fileToWrite; private final String fileContent; CacheWriter(FileManager fileManager, File fileToWrite, String fileContent) { this.fileManager = fileManager; this.fileToWrite = fileToWrite; this.fileContent = fileContent; } @Override public void run() { this.fileManager.writeToFile(fileToWrite, fileContent); } } /** * {@link Runnable} class for evicting all the cached files */ private static class CacheEvictor implements Runnable { private final FileManager fileManager; private final File cacheDir; CacheEvictor(FileManager fileManager, File cacheDir) { this.fileManager = fileManager; this.cacheDir = cacheDir; } @Override public void run() { this.fileManager.clearDirectory(this.cacheDir); } } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/cache/serializer/Serializer.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.cache.serializer; import com.google.gson.Gson; import javax.inject.Inject; import javax.inject.Singleton; /** * Json Serializer/Deserializer. */ @Singleton public class Serializer { private final Gson gson = new Gson(); @Inject Serializer() {} /** * Serialize an object to Json. * * @param object to serialize. */ public String serialize(Object object, Class clazz) { return gson.toJson(object, clazz); } /** * Deserialize a json representation of an object. * * @param string A json string to deserialize. */ public T deserialize(String string, Class clazz) { return gson.fromJson(string, clazz); } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/entity/UserEntity.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.entity; import com.google.gson.annotations.SerializedName; /** * User Entity used in the data layer. */ public class UserEntity { @SerializedName("id") private int userId; @SerializedName("cover_url") private String coverUrl; @SerializedName("full_name") private String fullname; @SerializedName("description") private String description; @SerializedName("followers") private int followers; @SerializedName("email") private String email; public UserEntity() { //empty } public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public String getCoverUrl() { return coverUrl; } public String getFullname() { return fullname; } public void setFullname(String fullname) { this.fullname = fullname; } public String getDescription() { return description; } public int getFollowers() { return followers; } public String getEmail() { return email; } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/entity/mapper/UserEntityDataMapper.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.entity.mapper; import com.fernandocejas.android10.sample.data.entity.UserEntity; import com.fernandocejas.android10.sample.domain.User; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; /** * Mapper class used to transform {@link UserEntity} (in the data layer) to {@link User} in the * domain layer. */ @Singleton public class UserEntityDataMapper { @Inject UserEntityDataMapper() {} /** * Transform a {@link UserEntity} into an {@link User}. * * @param userEntity Object to be transformed. * @return {@link User} if valid {@link UserEntity} otherwise null. */ public User transform(UserEntity userEntity) { User user = null; if (userEntity != null) { user = new User(userEntity.getUserId()); user.setCoverUrl(userEntity.getCoverUrl()); user.setFullName(userEntity.getFullname()); user.setDescription(userEntity.getDescription()); user.setFollowers(userEntity.getFollowers()); user.setEmail(userEntity.getEmail()); } return user; } /** * Transform a List of {@link UserEntity} into a Collection of {@link User}. * * @param userEntityCollection Object Collection to be transformed. * @return {@link User} if valid {@link UserEntity} otherwise null. */ public List transform(Collection userEntityCollection) { final List userList = new ArrayList<>(20); for (UserEntity userEntity : userEntityCollection) { final User user = transform(userEntity); if (user != null) { userList.add(user); } } return userList; } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/entity/mapper/UserEntityJsonMapper.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.entity.mapper; import com.fernandocejas.android10.sample.data.entity.UserEntity; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.List; import javax.inject.Inject; /** * Class used to transform from Strings representing json to valid objects. */ public class UserEntityJsonMapper { private final Gson gson; @Inject public UserEntityJsonMapper() { this.gson = new Gson(); } /** * Transform from valid json string to {@link UserEntity}. * * @param userJsonResponse A json representing a user profile. * @return {@link UserEntity}. * @throws com.google.gson.JsonSyntaxException if the json string is not a valid json structure. */ public UserEntity transformUserEntity(String userJsonResponse) throws JsonSyntaxException { final Type userEntityType = new TypeToken() {}.getType(); return this.gson.fromJson(userJsonResponse, userEntityType); } /** * Transform from valid json string to List of {@link UserEntity}. * * @param userListJsonResponse A json representing a collection of users. * @return List of {@link UserEntity}. * @throws com.google.gson.JsonSyntaxException if the json string is not a valid json structure. */ public List transformUserEntityCollection(String userListJsonResponse) throws JsonSyntaxException { final Type listOfUserEntityType = new TypeToken>() {}.getType(); return this.gson.fromJson(userListJsonResponse, listOfUserEntityType); } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/exception/NetworkConnectionException.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.exception; /** * Exception throw by the application when a there is a network connection exception. */ public class NetworkConnectionException extends Exception { public NetworkConnectionException() { super(); } public NetworkConnectionException(final Throwable cause) { super(cause); } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/exception/RepositoryErrorBundle.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.exception; import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; /** * Wrapper around Exceptions used to manage errors in the repository. */ class RepositoryErrorBundle implements ErrorBundle { private final Exception exception; RepositoryErrorBundle(Exception exception) { this.exception = exception; } @Override public Exception getException() { return exception; } @Override public String getErrorMessage() { String message = ""; if (this.exception != null) { message = this.exception.getMessage(); } return message; } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/exception/UserNotFoundException.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.exception; /** * Exception throw by the application when a User search can't return a valid result. */ public class UserNotFoundException extends Exception { public UserNotFoundException() { super(); } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/executor/JobExecutor.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.executor; import android.support.annotation.NonNull; import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Singleton; /** * Decorated {@link java.util.concurrent.ThreadPoolExecutor} */ @Singleton public class JobExecutor implements ThreadExecutor { private final ThreadPoolExecutor threadPoolExecutor; @Inject JobExecutor() { this.threadPoolExecutor = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new JobThreadFactory()); } @Override public void execute(@NonNull Runnable runnable) { this.threadPoolExecutor.execute(runnable); } private static class JobThreadFactory implements ThreadFactory { private int counter = 0; @Override public Thread newThread(@NonNull Runnable runnable) { return new Thread(runnable, "android_" + counter++); } } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/net/ApiConnection.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.net; import android.support.annotation.Nullable; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; /** * Api Connection class used to retrieve data from the cloud. * Implements {@link java.util.concurrent.Callable} so when executed asynchronously can * return a value. */ class ApiConnection implements Callable { private static final String CONTENT_TYPE_LABEL = "Content-Type"; private static final String CONTENT_TYPE_VALUE_JSON = "application/json; charset=utf-8"; private URL url; private String response; private ApiConnection(String url) throws MalformedURLException { this.url = new URL(url); } static ApiConnection createGET(String url) throws MalformedURLException { return new ApiConnection(url); } /** * Do a request to an api synchronously. * It should not be executed in the main thread of the application. * * @return A string response */ @Nullable String requestSyncCall() { connectToApi(); return response; } private void connectToApi() { OkHttpClient okHttpClient = this.createClient(); final Request request = new Request.Builder() .url(this.url) .addHeader(CONTENT_TYPE_LABEL, CONTENT_TYPE_VALUE_JSON) .get() .build(); try { this.response = okHttpClient.newCall(request).execute().body().string(); } catch (IOException e) { e.printStackTrace(); } } private OkHttpClient createClient() { final OkHttpClient okHttpClient = new OkHttpClient(); okHttpClient.setReadTimeout(10000, TimeUnit.MILLISECONDS); okHttpClient.setConnectTimeout(15000, TimeUnit.MILLISECONDS); return okHttpClient; } @Override public String call() throws Exception { return requestSyncCall(); } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/net/RestApi.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.net; import com.fernandocejas.android10.sample.data.entity.UserEntity; import io.reactivex.Observable; import java.util.List; /** * RestApi for retrieving data from the network. */ public interface RestApi { String API_BASE_URL = "https://raw.githubusercontent.com/android10/Sample-Data/master/Android-CleanArchitecture/"; /** Api url for getting all users */ String API_URL_GET_USER_LIST = API_BASE_URL + "users.json"; /** Api url for getting a user profile: Remember to concatenate id + 'json' */ String API_URL_GET_USER_DETAILS = API_BASE_URL + "user_"; /** * Retrieves an {@link Observable} which will emit a List of {@link UserEntity}. */ Observable> userEntityList(); /** * Retrieves an {@link Observable} which will emit a {@link UserEntity}. * * @param userId The user id used to get user data. */ Observable userEntityById(final int userId); } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/net/RestApiImpl.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.net; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import com.fernandocejas.android10.sample.data.entity.UserEntity; import com.fernandocejas.android10.sample.data.entity.mapper.UserEntityJsonMapper; import com.fernandocejas.android10.sample.data.exception.NetworkConnectionException; import io.reactivex.Observable; import java.net.MalformedURLException; import java.util.List; /** * {@link RestApi} implementation for retrieving data from the network. */ public class RestApiImpl implements RestApi { private final Context context; private final UserEntityJsonMapper userEntityJsonMapper; /** * Constructor of the class * * @param context {@link android.content.Context}. * @param userEntityJsonMapper {@link UserEntityJsonMapper}. */ public RestApiImpl(Context context, UserEntityJsonMapper userEntityJsonMapper) { if (context == null || userEntityJsonMapper == null) { throw new IllegalArgumentException("The constructor parameters cannot be null!!!"); } this.context = context.getApplicationContext(); this.userEntityJsonMapper = userEntityJsonMapper; } @Override public Observable> userEntityList() { return Observable.create(emitter -> { if (isThereInternetConnection()) { try { String responseUserEntities = getUserEntitiesFromApi(); if (responseUserEntities != null) { emitter.onNext(userEntityJsonMapper.transformUserEntityCollection( responseUserEntities)); emitter.onComplete(); } else { emitter.onError(new NetworkConnectionException()); } } catch (Exception e) { emitter.onError(new NetworkConnectionException(e.getCause())); } } else { emitter.onError(new NetworkConnectionException()); } }); } @Override public Observable userEntityById(final int userId) { return Observable.create(emitter -> { if (isThereInternetConnection()) { try { String responseUserDetails = getUserDetailsFromApi(userId); if (responseUserDetails != null) { emitter.onNext(userEntityJsonMapper.transformUserEntity(responseUserDetails)); emitter.onComplete(); } else { emitter.onError(new NetworkConnectionException()); } } catch (Exception e) { emitter.onError(new NetworkConnectionException(e.getCause())); } } else { emitter.onError(new NetworkConnectionException()); } }); } private String getUserEntitiesFromApi() throws MalformedURLException { return ApiConnection.createGET(API_URL_GET_USER_LIST).requestSyncCall(); } private String getUserDetailsFromApi(int userId) throws MalformedURLException { String apiUrl = API_URL_GET_USER_DETAILS + userId + ".json"; return ApiConnection.createGET(apiUrl).requestSyncCall(); } /** * Checks if the device has any active internet connection. * * @return true device with internet connection, otherwise false. */ private boolean isThereInternetConnection() { boolean isConnected; ConnectivityManager connectivityManager = (ConnectivityManager) this.context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); isConnected = (networkInfo != null && networkInfo.isConnectedOrConnecting()); return isConnected; } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/repository/UserDataRepository.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.repository; import com.fernandocejas.android10.sample.data.entity.mapper.UserEntityDataMapper; import com.fernandocejas.android10.sample.data.repository.datasource.UserDataStore; import com.fernandocejas.android10.sample.data.repository.datasource.UserDataStoreFactory; import com.fernandocejas.android10.sample.domain.User; import com.fernandocejas.android10.sample.domain.repository.UserRepository; import io.reactivex.Observable; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; /** * {@link UserRepository} for retrieving user data. */ @Singleton public class UserDataRepository implements UserRepository { private final UserDataStoreFactory userDataStoreFactory; private final UserEntityDataMapper userEntityDataMapper; /** * Constructs a {@link UserRepository}. * * @param dataStoreFactory A factory to construct different data source implementations. * @param userEntityDataMapper {@link UserEntityDataMapper}. */ @Inject UserDataRepository(UserDataStoreFactory dataStoreFactory, UserEntityDataMapper userEntityDataMapper) { this.userDataStoreFactory = dataStoreFactory; this.userEntityDataMapper = userEntityDataMapper; } @Override public Observable> users() { //we always get all users from the cloud final UserDataStore userDataStore = this.userDataStoreFactory.createCloudDataStore(); return userDataStore.userEntityList().map(this.userEntityDataMapper::transform); } @Override public Observable user(int userId) { final UserDataStore userDataStore = this.userDataStoreFactory.create(userId); return userDataStore.userEntityDetails(userId).map(this.userEntityDataMapper::transform); } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/repository/datasource/CloudUserDataStore.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.repository.datasource; import com.fernandocejas.android10.sample.data.cache.UserCache; import com.fernandocejas.android10.sample.data.entity.UserEntity; import com.fernandocejas.android10.sample.data.net.RestApi; import io.reactivex.Observable; import java.util.List; /** * {@link UserDataStore} implementation based on connections to the api (Cloud). */ class CloudUserDataStore implements UserDataStore { private final RestApi restApi; private final UserCache userCache; /** * Construct a {@link UserDataStore} based on connections to the api (Cloud). * * @param restApi The {@link RestApi} implementation to use. * @param userCache A {@link UserCache} to cache data retrieved from the api. */ CloudUserDataStore(RestApi restApi, UserCache userCache) { this.restApi = restApi; this.userCache = userCache; } @Override public Observable> userEntityList() { return this.restApi.userEntityList(); } @Override public Observable userEntityDetails(final int userId) { return this.restApi.userEntityById(userId).doOnNext(CloudUserDataStore.this.userCache::put); } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/repository/datasource/DiskUserDataStore.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.repository.datasource; import com.fernandocejas.android10.sample.data.cache.UserCache; import com.fernandocejas.android10.sample.data.entity.UserEntity; import io.reactivex.Observable; import java.util.List; /** * {@link UserDataStore} implementation based on file system data store. */ class DiskUserDataStore implements UserDataStore { private final UserCache userCache; /** * Construct a {@link UserDataStore} based file system data store. * * @param userCache A {@link UserCache} to cache data retrieved from the api. */ DiskUserDataStore(UserCache userCache) { this.userCache = userCache; } @Override public Observable> userEntityList() { //TODO: implement simple cache for storing/retrieving collections of users. throw new UnsupportedOperationException("Operation is not available!!!"); } @Override public Observable userEntityDetails(final int userId) { return this.userCache.get(userId); } } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/repository/datasource/UserDataStore.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.repository.datasource; import com.fernandocejas.android10.sample.data.entity.UserEntity; import io.reactivex.Observable; import java.util.List; /** * Interface that represents a data store from where data is retrieved. */ public interface UserDataStore { /** * Get an {@link Observable} which will emit a List of {@link UserEntity}. */ Observable> userEntityList(); /** * Get an {@link Observable} which will emit a {@link UserEntity} by its id. * * @param userId The id to retrieve user data. */ Observable userEntityDetails(final int userId); } ================================================ FILE: data/src/main/java/com/fernandocejas/android10/sample/data/repository/datasource/UserDataStoreFactory.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.repository.datasource; import android.content.Context; import android.support.annotation.NonNull; import com.fernandocejas.android10.sample.data.cache.UserCache; import com.fernandocejas.android10.sample.data.entity.mapper.UserEntityJsonMapper; import com.fernandocejas.android10.sample.data.net.RestApi; import com.fernandocejas.android10.sample.data.net.RestApiImpl; import javax.inject.Inject; import javax.inject.Singleton; /** * Factory that creates different implementations of {@link UserDataStore}. */ @Singleton public class UserDataStoreFactory { private final Context context; private final UserCache userCache; @Inject UserDataStoreFactory(@NonNull Context context, @NonNull UserCache userCache) { this.context = context.getApplicationContext(); this.userCache = userCache; } /** * Create {@link UserDataStore} from a user id. */ public UserDataStore create(int userId) { UserDataStore userDataStore; if (!this.userCache.isExpired() && this.userCache.isCached(userId)) { userDataStore = new DiskUserDataStore(this.userCache); } else { userDataStore = createCloudDataStore(); } return userDataStore; } /** * Create {@link UserDataStore} to retrieve data from the Cloud. */ public UserDataStore createCloudDataStore() { final UserEntityJsonMapper userEntityJsonMapper = new UserEntityJsonMapper(); final RestApi restApi = new RestApiImpl(this.context, userEntityJsonMapper); return new CloudUserDataStore(restApi, this.userCache); } } ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/ApplicationStub.java ================================================ /** * Copyright (C) 2015 android10.org Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data; import android.app.Application; public class ApplicationStub extends Application {} ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/ApplicationTestCase.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data; import android.content.Context; import java.io.File; import org.junit.Rule; import org.junit.rules.TestRule; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; /** * Base class for Robolectric data layer tests. * Inherit from this class to create a test. */ @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, application = ApplicationStub.class, sdk = 21) public abstract class ApplicationTestCase { @Rule public TestRule injectMocksRule = (base, description) -> { MockitoAnnotations.initMocks(ApplicationTestCase.this); return base; }; public static Context context() { return RuntimeEnvironment.application; } public static File cacheDir() { return RuntimeEnvironment.application.getCacheDir(); } } ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/cache/FileManagerTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.cache; import com.fernandocejas.android10.sample.data.ApplicationTestCase; import java.io.File; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class FileManagerTest extends ApplicationTestCase { private FileManager fileManager; @Before public void setUp() { fileManager = new FileManager(); } @After public void tearDown() { if (cacheDir() != null) { fileManager.clearDirectory(cacheDir()); } } @Test public void testWriteToFile() { File fileToWrite = createDummyFile(); String fileContent = "content"; fileManager.writeToFile(fileToWrite, fileContent); assertThat(fileToWrite.exists(), is(true)); } @Test public void testFileContent() { File fileToWrite = createDummyFile(); String fileContent = "content\n"; fileManager.writeToFile(fileToWrite, fileContent); String expectedContent = fileManager.readFileContent(fileToWrite); assertThat(expectedContent, is(equalTo(fileContent))); } private File createDummyFile() { String dummyFilePath = cacheDir().getPath() + File.separator + "dummyFile"; return new File(dummyFilePath); } } ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/cache/serializer/SerializerTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.cache.serializer; import com.fernandocejas.android10.sample.data.entity.UserEntity; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.runners.MockitoJUnitRunner; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @RunWith(MockitoJUnitRunner.class) public class SerializerTest { private static final String JSON_RESPONSE = "{\n" + " \"id\": 1,\n" + " \"cover_url\": \"http://www.android10.org/myapi/cover_1.jpg\",\n" + " \"full_name\": \"Simon Hill\",\n" + " \"description\": \"Curabitur gravida nisi at nibh. In hac habitasse platea dictumst. Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.\\n\\nInteger tincidunt ante vel ipsum. Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo placerat.\\n\\nPraesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.\",\n" + " \"followers\": 7484,\n" + " \"email\": \"jcooper@babbleset.edu\"\n" + "}"; private Serializer serializer; @Before public void setUp() { serializer = new Serializer(); } @Test public void testSerializeHappyCase() { final UserEntity userEntityOne = serializer.deserialize(JSON_RESPONSE, UserEntity.class); final String jsonString = serializer.serialize(userEntityOne, UserEntity.class); final UserEntity userEntityTwo = serializer.deserialize(jsonString, UserEntity.class); assertThat(userEntityOne.getUserId(), is(userEntityTwo.getUserId())); assertThat(userEntityOne.getFullname(), is(equalTo(userEntityTwo.getFullname()))); assertThat(userEntityOne.getFollowers(), is(userEntityTwo.getFollowers())); } @Test public void testDesearializeHappyCase() { final UserEntity userEntity = serializer.deserialize(JSON_RESPONSE, UserEntity.class); assertThat(userEntity.getUserId(), is(1)); assertThat(userEntity.getFullname(), is("Simon Hill")); assertThat(userEntity.getFollowers(), is(7484)); } } ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/entity/mapper/UserEntityDataMapperTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.entity.mapper; import com.fernandocejas.android10.sample.data.entity.UserEntity; import com.fernandocejas.android10.sample.domain.User; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.runners.MockitoJUnitRunner; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; @RunWith(MockitoJUnitRunner.class) public class UserEntityDataMapperTest { private static final int FAKE_USER_ID = 123; private static final String FAKE_FULLNAME = "Tony Stark"; private UserEntityDataMapper userEntityDataMapper; @Before public void setUp() throws Exception { userEntityDataMapper = new UserEntityDataMapper(); } @Test public void testTransformUserEntity() { UserEntity userEntity = createFakeUserEntity(); User user = userEntityDataMapper.transform(userEntity); assertThat(user, is(instanceOf(User.class))); assertThat(user.getUserId(), is(FAKE_USER_ID)); assertThat(user.getFullName(), is(FAKE_FULLNAME)); } @Test public void testTransformUserEntityCollection() { UserEntity mockUserEntityOne = mock(UserEntity.class); UserEntity mockUserEntityTwo = mock(UserEntity.class); List userEntityList = new ArrayList(5); userEntityList.add(mockUserEntityOne); userEntityList.add(mockUserEntityTwo); Collection userCollection = userEntityDataMapper.transform(userEntityList); assertThat(userCollection.toArray()[0], is(instanceOf(User.class))); assertThat(userCollection.toArray()[1], is(instanceOf(User.class))); assertThat(userCollection.size(), is(2)); } private UserEntity createFakeUserEntity() { UserEntity userEntity = new UserEntity(); userEntity.setUserId(FAKE_USER_ID); userEntity.setFullname(FAKE_FULLNAME); return userEntity; } } ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/entity/mapper/UserEntityJsonMapperTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.entity.mapper; import com.fernandocejas.android10.sample.data.entity.UserEntity; import com.google.gson.JsonSyntaxException; import java.util.Collection; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.runners.MockitoJUnitRunner; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @RunWith(MockitoJUnitRunner.class) public class UserEntityJsonMapperTest { private static final String JSON_RESPONSE_USER_DETAILS = "{\n" + " \"id\": 1,\n" + " \"cover_url\": \"http://www.android10.org/myapi/cover_1.jpg\",\n" + " \"full_name\": \"Simon Hill\",\n" + " \"description\": \"Curabitur gravida nisi at nibh. In hac habitasse platea dictumst. Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.\\n\\nInteger tincidunt ante vel ipsum. Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo placerat.\\n\\nPraesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.\",\n" + " \"followers\": 7484,\n" + " \"email\": \"jcooper@babbleset.edu\"\n" + "}"; private static final String JSON_RESPONSE_USER_COLLECTION = "[{\n" + " \"id\": 1,\n" + " \"full_name\": \"Simon Hill\",\n" + " \"followers\": 7484\n" + "}, {\n" + " \"id\": 12,\n" + " \"full_name\": \"Pedro Garcia\",\n" + " \"followers\": 1381\n" + "}]"; private UserEntityJsonMapper userEntityJsonMapper; @Rule public ExpectedException expectedException = ExpectedException.none(); @Before public void setUp() { userEntityJsonMapper = new UserEntityJsonMapper(); } @Test public void testTransformUserEntityHappyCase() { UserEntity userEntity = userEntityJsonMapper.transformUserEntity(JSON_RESPONSE_USER_DETAILS); assertThat(userEntity.getUserId(), is(1)); assertThat(userEntity.getFullname(), is(equalTo("Simon Hill"))); assertThat(userEntity.getEmail(), is(equalTo("jcooper@babbleset.edu"))); } @Test public void testTransformUserEntityCollectionHappyCase() { Collection userEntityCollection = userEntityJsonMapper.transformUserEntityCollection( JSON_RESPONSE_USER_COLLECTION); assertThat(((UserEntity) userEntityCollection.toArray()[0]).getUserId(), is(1)); assertThat(((UserEntity) userEntityCollection.toArray()[1]).getUserId(), is(12)); assertThat(userEntityCollection.size(), is(2)); } @Test public void testTransformUserEntityNotValidResponse() { expectedException.expect(JsonSyntaxException.class); userEntityJsonMapper.transformUserEntity("ironman"); } @Test public void testTransformUserEntityCollectionNotValidResponse() { expectedException.expect(JsonSyntaxException.class); userEntityJsonMapper.transformUserEntityCollection("Tony Stark"); } } ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/exception/RepositoryErrorBundleTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.exception; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class RepositoryErrorBundleTest { private RepositoryErrorBundle repositoryErrorBundle; @Mock private Exception mockException; @Before public void setUp() { repositoryErrorBundle = new RepositoryErrorBundle(mockException); } @Test @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public void testGetErrorMessageInteraction() { repositoryErrorBundle.getErrorMessage(); verify(mockException).getMessage(); } } ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/repository/UserDataRepositoryTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.repository; import com.fernandocejas.android10.sample.data.entity.UserEntity; import com.fernandocejas.android10.sample.data.entity.mapper.UserEntityDataMapper; import com.fernandocejas.android10.sample.data.repository.datasource.UserDataStore; import com.fernandocejas.android10.sample.data.repository.datasource.UserDataStoreFactory; import com.fernandocejas.android10.sample.domain.User; import io.reactivex.Observable; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class UserDataRepositoryTest { private static final int FAKE_USER_ID = 123; private UserDataRepository userDataRepository; @Mock private UserDataStoreFactory mockUserDataStoreFactory; @Mock private UserEntityDataMapper mockUserEntityDataMapper; @Mock private UserDataStore mockUserDataStore; @Mock private UserEntity mockUserEntity; @Mock private User mockUser; @Before public void setUp() { userDataRepository = new UserDataRepository(mockUserDataStoreFactory, mockUserEntityDataMapper); given(mockUserDataStoreFactory.create(anyInt())).willReturn(mockUserDataStore); given(mockUserDataStoreFactory.createCloudDataStore()).willReturn(mockUserDataStore); } @Test public void testGetUsersHappyCase() { List usersList = new ArrayList<>(); usersList.add(new UserEntity()); given(mockUserDataStore.userEntityList()).willReturn(Observable.just(usersList)); userDataRepository.users(); verify(mockUserDataStoreFactory).createCloudDataStore(); verify(mockUserDataStore).userEntityList(); } @Test public void testGetUserHappyCase() { UserEntity userEntity = new UserEntity(); given(mockUserDataStore.userEntityDetails(FAKE_USER_ID)).willReturn(Observable.just(userEntity)); userDataRepository.user(FAKE_USER_ID); verify(mockUserDataStoreFactory).create(FAKE_USER_ID); verify(mockUserDataStore).userEntityDetails(FAKE_USER_ID); } } ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/repository/datasource/CloudUserDataStoreTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.repository.datasource; import com.fernandocejas.android10.sample.data.cache.UserCache; import com.fernandocejas.android10.sample.data.entity.UserEntity; import com.fernandocejas.android10.sample.data.net.RestApi; import io.reactivex.Observable; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class CloudUserDataStoreTest { private static final int FAKE_USER_ID = 765; private CloudUserDataStore cloudUserDataStore; @Mock private RestApi mockRestApi; @Mock private UserCache mockUserCache; @Before public void setUp() { cloudUserDataStore = new CloudUserDataStore(mockRestApi, mockUserCache); } @Test public void testGetUserEntityListFromApi() { cloudUserDataStore.userEntityList(); verify(mockRestApi).userEntityList(); } @Test public void testGetUserEntityDetailsFromApi() { UserEntity fakeUserEntity = new UserEntity(); Observable fakeObservable = Observable.just(fakeUserEntity); given(mockRestApi.userEntityById(FAKE_USER_ID)).willReturn(fakeObservable); cloudUserDataStore.userEntityDetails(FAKE_USER_ID); verify(mockRestApi).userEntityById(FAKE_USER_ID); } } ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/repository/datasource/DiskUserDataStoreTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.repository.datasource; import com.fernandocejas.android10.sample.data.cache.UserCache; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class DiskUserDataStoreTest { private static final int FAKE_USER_ID = 11; private DiskUserDataStore diskUserDataStore; @Mock private UserCache mockUserCache; @Rule public ExpectedException expectedException = ExpectedException.none(); @Before public void setUp() { diskUserDataStore = new DiskUserDataStore(mockUserCache); } @Test public void testGetUserEntityListUnsupported() { expectedException.expect(UnsupportedOperationException.class); diskUserDataStore.userEntityList(); } @Test public void testGetUserEntityDetailesFromCache() { diskUserDataStore.userEntityDetails(FAKE_USER_ID); verify(mockUserCache).get(FAKE_USER_ID); } } ================================================ FILE: data/src/test/java/com/fernandocejas/android10/sample/data/repository/datasource/UserDataStoreFactoryTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.data.repository.datasource; import com.fernandocejas.android10.sample.data.ApplicationTestCase; import com.fernandocejas.android10.sample.data.cache.UserCache; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.robolectric.RuntimeEnvironment; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; public class UserDataStoreFactoryTest extends ApplicationTestCase { private static final int FAKE_USER_ID = 11; private UserDataStoreFactory userDataStoreFactory; @Mock private UserCache mockUserCache; @Before public void setUp() { userDataStoreFactory = new UserDataStoreFactory(RuntimeEnvironment.application, mockUserCache); } @Test public void testCreateDiskDataStore() { given(mockUserCache.isCached(FAKE_USER_ID)).willReturn(true); given(mockUserCache.isExpired()).willReturn(false); UserDataStore userDataStore = userDataStoreFactory.create(FAKE_USER_ID); assertThat(userDataStore, is(notNullValue())); assertThat(userDataStore, is(instanceOf(DiskUserDataStore.class))); verify(mockUserCache).isCached(FAKE_USER_ID); verify(mockUserCache).isExpired(); } @Test public void testCreateCloudDataStore() { given(mockUserCache.isExpired()).willReturn(true); given(mockUserCache.isCached(FAKE_USER_ID)).willReturn(false); UserDataStore userDataStore = userDataStoreFactory.create(FAKE_USER_ID); assertThat(userDataStore, is(notNullValue())); assertThat(userDataStore, is(instanceOf(CloudUserDataStore.class))); verify(mockUserCache).isExpired(); } } ================================================ FILE: domain/build.gradle ================================================ apply plugin: 'java' //noinspection GroovyUnusedAssignment sourceCompatibility = 1.7 //noinspection GroovyUnusedAssignment targetCompatibility = 1.7 configurations { provided } sourceSets { main { compileClasspath += configurations.provided } } dependencies { def domainDependencies = rootProject.ext.domainDependencies def domainTestDependencies = rootProject.ext.domainTestDependencies compileOnly domainDependencies.javaxAnnotation implementation domainDependencies.javaxInject implementation domainDependencies.rxJava compile domainDependencies.arrow testImplementation domainTestDependencies.junit testImplementation domainTestDependencies.mockito testImplementation domainTestDependencies.assertj } ================================================ FILE: domain/src/main/java/com/fernandocejas/android10/sample/domain/User.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain; /** * Class that represents a User in the domain layer. */ public class User { private final int userId; public User(int userId) { this.userId = userId; } private String coverUrl; private String fullName; private String email; private String description; private int followers; public int getUserId() { return userId; } public String getCoverUrl() { return coverUrl; } public void setCoverUrl(String coverUrl) { this.coverUrl = coverUrl; } public String getFullName() { return fullName; } public void setFullName(String fullName) { this.fullName = fullName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public int getFollowers() { return followers; } public void setFollowers(int followers) { this.followers = followers; } } ================================================ FILE: domain/src/main/java/com/fernandocejas/android10/sample/domain/exception/DefaultErrorBundle.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.exception; /** * Wrapper around Exceptions used to manage default errors. */ public class DefaultErrorBundle implements ErrorBundle { private static final String DEFAULT_ERROR_MSG = "Unknown error"; private final Exception exception; public DefaultErrorBundle(Exception exception) { this.exception = exception; } @Override public Exception getException() { return exception; } @Override public String getErrorMessage() { return (exception != null) ? this.exception.getMessage() : DEFAULT_ERROR_MSG; } } ================================================ FILE: domain/src/main/java/com/fernandocejas/android10/sample/domain/exception/ErrorBundle.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.exception; /** * Interface to represent a wrapper around an {@link java.lang.Exception} to manage errors. */ public interface ErrorBundle { Exception getException(); String getErrorMessage(); } ================================================ FILE: domain/src/main/java/com/fernandocejas/android10/sample/domain/executor/PostExecutionThread.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.executor; import io.reactivex.Scheduler; /** * Thread abstraction created to change the execution context from any thread to any other thread. * Useful to encapsulate a UI Thread for example, since some job will be done in background, an * implementation of this interface will change context and update the UI. */ public interface PostExecutionThread { Scheduler getScheduler(); } ================================================ FILE: domain/src/main/java/com/fernandocejas/android10/sample/domain/executor/ThreadExecutor.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.executor; import java.util.concurrent.Executor; /** * Executor implementation can be based on different frameworks or techniques of asynchronous * execution, but every implementation will execute the * {@link com.fernandocejas.android10.sample.domain.interactor.UseCase} out of the UI thread. */ public interface ThreadExecutor extends Executor {} ================================================ FILE: domain/src/main/java/com/fernandocejas/android10/sample/domain/interactor/DefaultObserver.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.interactor; import io.reactivex.observers.DisposableObserver; /** * Default {@link DisposableObserver} base class to be used whenever you want default error handling. */ public class DefaultObserver extends DisposableObserver { @Override public void onNext(T t) { // no-op by default. } @Override public void onComplete() { // no-op by default. } @Override public void onError(Throwable exception) { // no-op by default. } } ================================================ FILE: domain/src/main/java/com/fernandocejas/android10/sample/domain/interactor/GetUserDetails.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.interactor; import com.fernandocejas.android10.sample.domain.User; import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; import com.fernandocejas.android10.sample.domain.repository.UserRepository; import com.fernandocejas.arrow.checks.Preconditions; import io.reactivex.Observable; import javax.inject.Inject; /** * This class is an implementation of {@link UseCase} that represents a use case for * retrieving data related to an specific {@link User}. */ public class GetUserDetails extends UseCase { private final UserRepository userRepository; @Inject GetUserDetails(UserRepository userRepository, ThreadExecutor threadExecutor, PostExecutionThread postExecutionThread) { super(threadExecutor, postExecutionThread); this.userRepository = userRepository; } @Override Observable buildUseCaseObservable(Params params) { Preconditions.checkNotNull(params); return this.userRepository.user(params.userId); } public static final class Params { private final int userId; private Params(int userId) { this.userId = userId; } public static Params forUser(int userId) { return new Params(userId); } } } ================================================ FILE: domain/src/main/java/com/fernandocejas/android10/sample/domain/interactor/GetUserList.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.interactor; import com.fernandocejas.android10.sample.domain.User; import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; import com.fernandocejas.android10.sample.domain.repository.UserRepository; import io.reactivex.Observable; import java.util.List; import javax.inject.Inject; /** * This class is an implementation of {@link UseCase} that represents a use case for * retrieving a collection of all {@link User}. */ public class GetUserList extends UseCase, Void> { private final UserRepository userRepository; @Inject GetUserList(UserRepository userRepository, ThreadExecutor threadExecutor, PostExecutionThread postExecutionThread) { super(threadExecutor, postExecutionThread); this.userRepository = userRepository; } @Override Observable> buildUseCaseObservable(Void unused) { return this.userRepository.users(); } } ================================================ FILE: domain/src/main/java/com/fernandocejas/android10/sample/domain/interactor/UseCase.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.interactor; import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; import com.fernandocejas.arrow.checks.Preconditions; import io.reactivex.Observable; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.observers.DisposableObserver; import io.reactivex.schedulers.Schedulers; /** * Abstract class for a Use Case (Interactor in terms of Clean Architecture). * This interface represents a execution unit for different use cases (this means any use case * in the application should implement this contract). * * By convention each UseCase implementation will return the result using a {@link DisposableObserver} * that will execute its job in a background thread and will post the result in the UI thread. */ public abstract class UseCase { private final ThreadExecutor threadExecutor; private final PostExecutionThread postExecutionThread; private final CompositeDisposable disposables; UseCase(ThreadExecutor threadExecutor, PostExecutionThread postExecutionThread) { this.threadExecutor = threadExecutor; this.postExecutionThread = postExecutionThread; this.disposables = new CompositeDisposable(); } /** * Builds an {@link Observable} which will be used when executing the current {@link UseCase}. */ abstract Observable buildUseCaseObservable(Params params); /** * Executes the current use case. * * @param observer {@link DisposableObserver} which will be listening to the observable build * by {@link #buildUseCaseObservable(Params)} ()} method. * @param params Parameters (Optional) used to build/execute this use case. */ public void execute(DisposableObserver observer, Params params) { Preconditions.checkNotNull(observer); final Observable observable = this.buildUseCaseObservable(params) .subscribeOn(Schedulers.from(threadExecutor)) .observeOn(postExecutionThread.getScheduler()); addDisposable(observable.subscribeWith(observer)); } /** * Dispose from current {@link CompositeDisposable}. */ public void dispose() { if (!disposables.isDisposed()) { disposables.dispose(); } } /** * Dispose from current {@link CompositeDisposable}. */ private void addDisposable(Disposable disposable) { Preconditions.checkNotNull(disposable); Preconditions.checkNotNull(disposables); disposables.add(disposable); } } ================================================ FILE: domain/src/main/java/com/fernandocejas/android10/sample/domain/repository/UserRepository.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.repository; import com.fernandocejas.android10.sample.domain.User; import io.reactivex.Observable; import java.util.List; /** * Interface that represents a Repository for getting {@link User} related data. */ public interface UserRepository { /** * Get an {@link Observable} which will emit a List of {@link User}. */ Observable> users(); /** * Get an {@link Observable} which will emit a {@link User}. * * @param userId The user id used to retrieve user data. */ Observable user(final int userId); } ================================================ FILE: domain/src/test/java/com/fernandocejas/android10/sample/domain/UserTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain; import org.junit.Before; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class UserTest { private static final int FAKE_USER_ID = 8; private User user; @Before public void setUp() { user = new User(FAKE_USER_ID); } @Test public void testUserConstructorHappyCase() { final int userId = user.getUserId(); assertThat(userId).isEqualTo(FAKE_USER_ID); } } ================================================ FILE: domain/src/test/java/com/fernandocejas/android10/sample/domain/exception/DefaultErrorBundleTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.exception; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class DefaultErrorBundleTest { private DefaultErrorBundle defaultErrorBundle; @Mock private Exception mockException; @Before public void setUp() { defaultErrorBundle = new DefaultErrorBundle(mockException); } @Test public void testGetErrorMessageInteraction() { defaultErrorBundle.getErrorMessage(); verify(mockException).getMessage(); } } ================================================ FILE: domain/src/test/java/com/fernandocejas/android10/sample/domain/interactor/GetUserDetailsTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.interactor; import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; import com.fernandocejas.android10.sample.domain.interactor.GetUserDetails.Params; import com.fernandocejas.android10.sample.domain.repository.UserRepository; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @RunWith(MockitoJUnitRunner.class) public class GetUserDetailsTest { private static final int USER_ID = 123; private GetUserDetails getUserDetails; @Mock private UserRepository mockUserRepository; @Mock private ThreadExecutor mockThreadExecutor; @Mock private PostExecutionThread mockPostExecutionThread; @Rule public ExpectedException expectedException = ExpectedException.none(); @Before public void setUp() { getUserDetails = new GetUserDetails(mockUserRepository, mockThreadExecutor, mockPostExecutionThread); } @Test public void testGetUserDetailsUseCaseObservableHappyCase() { getUserDetails.buildUseCaseObservable(Params.forUser(USER_ID)); verify(mockUserRepository).user(USER_ID); verifyNoMoreInteractions(mockUserRepository); verifyZeroInteractions(mockPostExecutionThread); verifyZeroInteractions(mockThreadExecutor); } @Test public void testShouldFailWhenNoOrEmptyParameters() { expectedException.expect(NullPointerException.class); getUserDetails.buildUseCaseObservable(null); } } ================================================ FILE: domain/src/test/java/com/fernandocejas/android10/sample/domain/interactor/GetUserListTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.interactor; import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; import com.fernandocejas.android10.sample.domain.repository.UserRepository; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @RunWith(MockitoJUnitRunner.class) public class GetUserListTest { private GetUserList getUserList; @Mock private ThreadExecutor mockThreadExecutor; @Mock private PostExecutionThread mockPostExecutionThread; @Mock private UserRepository mockUserRepository; @Before public void setUp() { getUserList = new GetUserList(mockUserRepository, mockThreadExecutor, mockPostExecutionThread); } @Test public void testGetUserListUseCaseObservableHappyCase() { getUserList.buildUseCaseObservable(null); verify(mockUserRepository).users(); verifyNoMoreInteractions(mockUserRepository); verifyZeroInteractions(mockThreadExecutor); verifyZeroInteractions(mockPostExecutionThread); } } ================================================ FILE: domain/src/test/java/com/fernandocejas/android10/sample/domain/interactor/UseCaseTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.domain.interactor; import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; import io.reactivex.Observable; import io.reactivex.observers.DisposableObserver; import io.reactivex.schedulers.TestScheduler; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @RunWith(MockitoJUnitRunner.class) public class UseCaseTest { private UseCaseTestClass useCase; private TestDisposableObserver testObserver; @Mock private ThreadExecutor mockThreadExecutor; @Mock private PostExecutionThread mockPostExecutionThread; @Rule public ExpectedException expectedException = ExpectedException.none(); @Before public void setUp() { this.useCase = new UseCaseTestClass(mockThreadExecutor, mockPostExecutionThread); this.testObserver = new TestDisposableObserver<>(); given(mockPostExecutionThread.getScheduler()).willReturn(new TestScheduler()); } @Test public void testBuildUseCaseObservableReturnCorrectResult() { useCase.execute(testObserver, Params.EMPTY); assertThat(testObserver.valuesCount).isZero(); } @Test public void testSubscriptionWhenExecutingUseCase() { useCase.execute(testObserver, Params.EMPTY); useCase.dispose(); assertThat(testObserver.isDisposed()).isTrue(); } @Test public void testShouldFailWhenExecuteWithNullObserver() { expectedException.expect(NullPointerException.class); useCase.execute(null, Params.EMPTY); } private static class UseCaseTestClass extends UseCase { UseCaseTestClass(ThreadExecutor threadExecutor, PostExecutionThread postExecutionThread) { super(threadExecutor, postExecutionThread); } @Override Observable buildUseCaseObservable(Params params) { return Observable.empty(); } @Override public void execute(DisposableObserver observer, Params params) { super.execute(observer, params); } } private static class TestDisposableObserver extends DisposableObserver { private int valuesCount = 0; @Override public void onNext(T value) { valuesCount++; } @Override public void onError(Throwable e) { // no-op by default. } @Override public void onComplete() { // no-op by default. } } private static class Params { private static Params EMPTY = new Params(); private Params() {} } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Wed Dec 21 17:11:04 ART 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip ================================================ FILE: gradle.properties ================================================ #Gradle configuration org.gradle.daemon=true org.gradle.jvmargs=-Dfile.encoding=UTF-8 org.gradle.parallel=true org.gradle.configureondemand=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save ( ) { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: presentation/.gitignore ================================================ /build ================================================ FILE: presentation/build.gradle ================================================ apply plugin: 'com.android.application' //apply plugin: 'com.neenbedankt.android-apt' android { def globalConfiguration = rootProject.extensions.getByName("ext") compileSdkVersion globalConfiguration.getAt("androidCompileSdkVersion") buildToolsVersion globalConfiguration.getAt("androidBuildToolsVersion") defaultConfig { minSdkVersion globalConfiguration.getAt("androidMinSdkVersion") targetSdkVersion globalConfiguration.getAt("androidTargetSdkVersion") applicationId globalConfiguration.getAt("androidApplicationId") versionCode globalConfiguration.getAt("androidVersionCode") versionName globalConfiguration.getAt("androidVersionName") testInstrumentationRunner globalConfiguration.getAt("testInstrumentationRunner") testApplicationId globalConfiguration.getAt("testApplicationId") } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } packagingOptions { exclude 'LICENSE.txt' exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/ASL2.0' exclude 'META-INF/NOTICE' exclude 'META-INF/LICENSE' } lintOptions { quiet true abortOnError false ignoreWarnings true disable 'InvalidPackage' //Some libraries have issues with this. disable 'OldTargetApi' //Lint gives this warning but SDK 20 would be Android L Beta. disable 'IconDensities' //For testing purpose. This is safe to remove. disable 'IconMissingDensityFolder' //For testing purpose. This is safe to remove. } signingConfigs { debug { storeFile file('../buildsystem/debug.keystore') storePassword 'android' keyAlias 'androiddebugkey' keyPassword 'android' } } buildTypes { debug { signingConfig signingConfigs.debug } } } dependencies { def presentationDependencies = rootProject.ext.presentationDependencies def presentationTestDependencies = rootProject.ext.presentationTestDependencies def developmentDependencies = rootProject.ext.developmentDependencies implementation project(':domain') implementation project(':data') annotationProcessor presentationDependencies.daggerCompiler implementation presentationDependencies.dagger implementation presentationDependencies.butterKnife annotationProcessor presentationDependencies.butterKnife implementation presentationDependencies.recyclerView implementation presentationDependencies.rxJava implementation presentationDependencies.rxAndroid compileOnly presentationDependencies.javaxAnnotation androidTestImplementation presentationTestDependencies.mockito androidTestImplementation presentationTestDependencies.dexmaker androidTestImplementation presentationTestDependencies.dexmakerMockito androidTestImplementation presentationTestDependencies.espresso androidTestImplementation presentationTestDependencies.testingSupportLib //Development implementation developmentDependencies.leakCanary } repositories { google() } ================================================ FILE: presentation/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/fcejas/Software/SDKs/android-sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/exception/ErrorMessageFactoryTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.test.exception; import android.test.AndroidTestCase; import com.fernandocejas.android10.sample.data.exception.NetworkConnectionException; import com.fernandocejas.android10.sample.data.exception.UserNotFoundException; import com.fernandocejas.android10.sample.presentation.R; import com.fernandocejas.android10.sample.presentation.exception.ErrorMessageFactory; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class ErrorMessageFactoryTest extends AndroidTestCase { @Override protected void setUp() throws Exception { super.setUp(); } public void testNetworkConnectionErrorMessage() { String expectedMessage = getContext().getString(R.string.exception_message_no_connection); String actualMessage = ErrorMessageFactory.create(getContext(), new NetworkConnectionException()); assertThat(actualMessage, is(equalTo(expectedMessage))); } public void testUserNotFoundErrorMessage() { String expectedMessage = getContext().getString(R.string.exception_message_user_not_found); String actualMessage = ErrorMessageFactory.create(getContext(), new UserNotFoundException()); assertThat(actualMessage, is(equalTo(expectedMessage))); } } ================================================ FILE: presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/mapper/UserModelDataMapperTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.test.mapper; import com.fernandocejas.android10.sample.domain.User; import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; import com.fernandocejas.android10.sample.presentation.model.UserModel; import java.util.ArrayList; import java.util.Collection; import java.util.List; import junit.framework.TestCase; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; public class UserModelDataMapperTest extends TestCase { private static final int FAKE_USER_ID = 123; private static final String FAKE_FULL_NAME = "Tony Stark"; private UserModelDataMapper userModelDataMapper; @Override protected void setUp() throws Exception { super.setUp(); userModelDataMapper = new UserModelDataMapper(); } public void testTransformUser() { User user = createFakeUser(); UserModel userModel = userModelDataMapper.transform(user); assertThat(userModel, is(instanceOf(UserModel.class))); assertThat(userModel.getUserId(), is(FAKE_USER_ID)); assertThat(userModel.getFullName(), is(FAKE_FULL_NAME)); } public void testTransformUserCollection() { User mockUserOne = mock(User.class); User mockUserTwo = mock(User.class); List userList = new ArrayList(5); userList.add(mockUserOne); userList.add(mockUserTwo); Collection userModelList = userModelDataMapper.transform(userList); assertThat(userModelList.toArray()[0], is(instanceOf(UserModel.class))); assertThat(userModelList.toArray()[1], is(instanceOf(UserModel.class))); assertThat(userModelList.size(), is(2)); } private User createFakeUser() { User user = new User(FAKE_USER_ID); user.setFullName(FAKE_FULL_NAME); return user; } } ================================================ FILE: presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/presenter/UserDetailsPresenterTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.test.presenter; import android.content.Context; import com.fernandocejas.android10.sample.domain.interactor.GetUserDetails; import com.fernandocejas.android10.sample.domain.interactor.GetUserDetails.Params; import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; import com.fernandocejas.android10.sample.presentation.presenter.UserDetailsPresenter; import com.fernandocejas.android10.sample.presentation.view.UserDetailsView; import io.reactivex.observers.DisposableObserver; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class UserDetailsPresenterTest { private static final int USER_ID = 1; private UserDetailsPresenter userDetailsPresenter; @Mock private Context mockContext; @Mock private UserDetailsView mockUserDetailsView; @Mock private GetUserDetails mockGetUserDetails; @Mock private UserModelDataMapper mockUserModelDataMapper; @Before public void setUp() { userDetailsPresenter = new UserDetailsPresenter(mockGetUserDetails, mockUserModelDataMapper); userDetailsPresenter.setView(mockUserDetailsView); } @Test @SuppressWarnings("unchecked") public void testUserDetailsPresenterInitialize() { given(mockUserDetailsView.context()).willReturn(mockContext); userDetailsPresenter.initialize(USER_ID); verify(mockUserDetailsView).hideRetry(); verify(mockUserDetailsView).showLoading(); verify(mockGetUserDetails).execute(any(DisposableObserver.class), any(Params.class)); } } ================================================ FILE: presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/presenter/UserListPresenterTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.test.presenter; import android.content.Context; import com.fernandocejas.android10.sample.domain.interactor.GetUserList; import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; import com.fernandocejas.android10.sample.presentation.presenter.UserListPresenter; import com.fernandocejas.android10.sample.presentation.view.UserListView; import io.reactivex.observers.DisposableObserver; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class UserListPresenterTest { private UserListPresenter userListPresenter; @Mock private Context mockContext; @Mock private UserListView mockUserListView; @Mock private GetUserList mockGetUserList; @Mock private UserModelDataMapper mockUserModelDataMapper; @Before public void setUp() { userListPresenter = new UserListPresenter(mockGetUserList, mockUserModelDataMapper); userListPresenter.setView(mockUserListView); } @Test @SuppressWarnings("unchecked") public void testUserListPresenterInitialize() { given(mockUserListView.context()).willReturn(mockContext); userListPresenter.initialize(); verify(mockUserListView).hideRetry(); verify(mockUserListView).showLoading(); verify(mockGetUserList).execute(any(DisposableObserver.class), any(Void.class)); } } ================================================ FILE: presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/view/activity/UserDetailsActivityTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.test.view.activity; import android.app.Fragment; import android.content.Intent; import android.test.ActivityInstrumentationTestCase2; import com.fernandocejas.android10.sample.presentation.R; import com.fernandocejas.android10.sample.presentation.view.activity.UserDetailsActivity; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.not; public class UserDetailsActivityTest extends ActivityInstrumentationTestCase2 { private static final int FAKE_USER_ID = 10; private UserDetailsActivity userDetailsActivity; public UserDetailsActivityTest() { super(UserDetailsActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); this.setActivityIntent(createTargetIntent()); this.userDetailsActivity = getActivity(); } @Override protected void tearDown() throws Exception { super.tearDown(); } public void testContainsUserDetailsFragment() { Fragment userDetailsFragment = userDetailsActivity.getFragmentManager().findFragmentById(R.id.fragmentContainer); assertThat(userDetailsFragment, is(notNullValue())); } public void testContainsProperTitle() { String actualTitle = this.userDetailsActivity.getTitle().toString().trim(); assertThat(actualTitle, is("User Details")); } public void testLoadUserHappyCaseViews() { onView(withId(R.id.rl_retry)).check(matches(not(isDisplayed()))); onView(withId(R.id.rl_progress)).check(matches(not(isDisplayed()))); onView(withId(R.id.tv_fullname)).check(matches(isDisplayed())); onView(withId(R.id.tv_email)).check(matches(isDisplayed())); onView(withId(R.id.tv_description)).check(matches(isDisplayed())); } public void testLoadUserHappyCaseData() { onView(withId(R.id.tv_fullname)).check(matches(withText("John Sanchez"))); onView(withId(R.id.tv_email)).check(matches(withText("dmedina@katz.edu"))); onView(withId(R.id.tv_followers)).check(matches(withText("4523"))); } private Intent createTargetIntent() { Intent intentLaunchActivity = UserDetailsActivity.getCallingIntent(getInstrumentation().getTargetContext(), FAKE_USER_ID); return intentLaunchActivity; } } ================================================ FILE: presentation/src/androidTest/java/com/fernandocejas/android10/sample/test/view/activity/UserListActivityTest.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.test.view.activity; import android.app.Fragment; import android.content.Intent; import android.test.ActivityInstrumentationTestCase2; import com.fernandocejas.android10.sample.presentation.R; import com.fernandocejas.android10.sample.presentation.view.activity.UserListActivity; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; public class UserListActivityTest extends ActivityInstrumentationTestCase2 { private UserListActivity userListActivity; public UserListActivityTest() { super(UserListActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); this.setActivityIntent(createTargetIntent()); userListActivity = getActivity(); } @Override protected void tearDown() throws Exception { super.tearDown(); } public void testContainsUserListFragment() { Fragment userListFragment = userListActivity.getFragmentManager().findFragmentById(R.id.fragmentContainer); assertThat(userListFragment, is(notNullValue())); } public void testContainsProperTitle() { String actualTitle = this.userListActivity.getTitle().toString().trim(); assertThat(actualTitle, is("Users List")); } private Intent createTargetIntent() { Intent intentLaunchActivity = UserListActivity.getCallingIntent(getInstrumentation().getTargetContext()); return intentLaunchActivity; } } ================================================ FILE: presentation/src/main/AndroidManifest.xml ================================================ ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/AndroidApplication.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation; import android.app.Application; import com.fernandocejas.android10.sample.presentation.internal.di.components.ApplicationComponent; import com.fernandocejas.android10.sample.presentation.internal.di.components.DaggerApplicationComponent; import com.fernandocejas.android10.sample.presentation.internal.di.modules.ApplicationModule; import com.squareup.leakcanary.LeakCanary; /** * Android Main Application */ public class AndroidApplication extends Application { private ApplicationComponent applicationComponent; @Override public void onCreate() { super.onCreate(); this.initializeInjector(); this.initializeLeakDetection(); } private void initializeInjector() { this.applicationComponent = DaggerApplicationComponent.builder() .applicationModule(new ApplicationModule(this)) .build(); } public ApplicationComponent getApplicationComponent() { return this.applicationComponent; } private void initializeLeakDetection() { if (BuildConfig.DEBUG) { LeakCanary.install(this); } } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/UIThread.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation; import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; import javax.inject.Inject; import javax.inject.Singleton; /** * MainThread (UI Thread) implementation based on a {@link Scheduler} * which will execute actions on the Android UI thread */ @Singleton public class UIThread implements PostExecutionThread { @Inject UIThread() {} @Override public Scheduler getScheduler() { return AndroidSchedulers.mainThread(); } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/exception/ErrorMessageFactory.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.exception; import android.content.Context; import com.fernandocejas.android10.sample.data.exception.NetworkConnectionException; import com.fernandocejas.android10.sample.data.exception.UserNotFoundException; import com.fernandocejas.android10.sample.presentation.R; /** * Factory used to create error messages from an Exception as a condition. */ public class ErrorMessageFactory { private ErrorMessageFactory() { //empty } /** * Creates a String representing an error message. * * @param context Context needed to retrieve string resources. * @param exception An exception used as a condition to retrieve the correct error message. * @return {@link String} an error message. */ public static String create(Context context, Exception exception) { String message = context.getString(R.string.exception_message_generic); if (exception instanceof NetworkConnectionException) { message = context.getString(R.string.exception_message_no_connection); } else if (exception instanceof UserNotFoundException) { message = context.getString(R.string.exception_message_user_not_found); } return message; } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/internal/di/HasComponent.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.internal.di; /** * Interface representing a contract for clients that contains a component for dependency injection. */ public interface HasComponent { C getComponent(); } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/internal/di/PerActivity.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.internal.di; import java.lang.annotation.Retention; import javax.inject.Scope; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * A scoping annotation to permit objects whose lifetime should * conform to the life of the activity to be memorized in the * correct component. */ @Scope @Retention(RUNTIME) public @interface PerActivity {} ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/internal/di/components/ActivityComponent.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.internal.di.components; import android.app.Activity; import com.fernandocejas.android10.sample.presentation.internal.di.PerActivity; import com.fernandocejas.android10.sample.presentation.internal.di.modules.ActivityModule; import dagger.Component; /** * A base component upon which fragment's components may depend. * Activity-level components should extend this component. * * Subtypes of ActivityComponent should be decorated with annotation: * {@link com.fernandocejas.android10.sample.presentation.internal.di.PerActivity} */ @PerActivity @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) interface ActivityComponent { //Exposed to sub-graphs. Activity activity(); } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/internal/di/components/ApplicationComponent.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.internal.di.components; import android.content.Context; import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; import com.fernandocejas.android10.sample.domain.repository.UserRepository; import com.fernandocejas.android10.sample.presentation.internal.di.modules.ApplicationModule; import com.fernandocejas.android10.sample.presentation.view.activity.BaseActivity; import dagger.Component; import javax.inject.Singleton; /** * A component whose lifetime is the life of the application. */ @Singleton // Constraints this component to one-per-application or unscoped bindings. @Component(modules = ApplicationModule.class) public interface ApplicationComponent { void inject(BaseActivity baseActivity); //Exposed to sub-graphs. Context context(); ThreadExecutor threadExecutor(); PostExecutionThread postExecutionThread(); UserRepository userRepository(); } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/internal/di/components/UserComponent.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.internal.di.components; import com.fernandocejas.android10.sample.presentation.internal.di.PerActivity; import com.fernandocejas.android10.sample.presentation.internal.di.modules.ActivityModule; import com.fernandocejas.android10.sample.presentation.internal.di.modules.UserModule; import com.fernandocejas.android10.sample.presentation.view.fragment.UserDetailsFragment; import com.fernandocejas.android10.sample.presentation.view.fragment.UserListFragment; import dagger.Component; /** * A scope {@link com.fernandocejas.android10.sample.presentation.internal.di.PerActivity} component. * Injects user specific Fragments. */ @PerActivity @Component(dependencies = ApplicationComponent.class, modules = {ActivityModule.class, UserModule.class}) public interface UserComponent extends ActivityComponent { void inject(UserListFragment userListFragment); void inject(UserDetailsFragment userDetailsFragment); } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/internal/di/modules/ActivityModule.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.internal.di.modules; import android.app.Activity; import com.fernandocejas.android10.sample.presentation.internal.di.PerActivity; import dagger.Module; import dagger.Provides; /** * A module to wrap the Activity state and expose it to the graph. */ @Module public class ActivityModule { private final Activity activity; public ActivityModule(Activity activity) { this.activity = activity; } /** * Expose the activity to dependents in the graph. */ @Provides @PerActivity Activity activity() { return this.activity; } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/internal/di/modules/ApplicationModule.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.internal.di.modules; import android.content.Context; import com.fernandocejas.android10.sample.data.cache.UserCache; import com.fernandocejas.android10.sample.data.cache.UserCacheImpl; import com.fernandocejas.android10.sample.data.executor.JobExecutor; import com.fernandocejas.android10.sample.data.repository.UserDataRepository; import com.fernandocejas.android10.sample.domain.executor.PostExecutionThread; import com.fernandocejas.android10.sample.domain.executor.ThreadExecutor; import com.fernandocejas.android10.sample.domain.repository.UserRepository; import com.fernandocejas.android10.sample.presentation.AndroidApplication; import com.fernandocejas.android10.sample.presentation.UIThread; import com.fernandocejas.android10.sample.presentation.navigation.Navigator; import dagger.Module; import dagger.Provides; import javax.inject.Singleton; /** * Dagger module that provides objects which will live during the application lifecycle. */ @Module public class ApplicationModule { private final AndroidApplication application; public ApplicationModule(AndroidApplication application) { this.application = application; } @Provides @Singleton Context provideApplicationContext() { return this.application; } @Provides @Singleton ThreadExecutor provideThreadExecutor(JobExecutor jobExecutor) { return jobExecutor; } @Provides @Singleton PostExecutionThread providePostExecutionThread(UIThread uiThread) { return uiThread; } @Provides @Singleton UserCache provideUserCache(UserCacheImpl userCache) { return userCache; } @Provides @Singleton UserRepository provideUserRepository(UserDataRepository userDataRepository) { return userDataRepository; } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/internal/di/modules/UserModule.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.internal.di.modules; import dagger.Module; /** * Dagger module that provides user related collaborators. */ @Module public class UserModule { public UserModule() {} } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/mapper/UserModelDataMapper.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.mapper; import com.fernandocejas.android10.sample.domain.User; import com.fernandocejas.android10.sample.presentation.internal.di.PerActivity; import com.fernandocejas.android10.sample.presentation.model.UserModel; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import javax.inject.Inject; /** * Mapper class used to transform {@link User} (in the domain layer) to {@link UserModel} in the * presentation layer. */ @PerActivity public class UserModelDataMapper { @Inject public UserModelDataMapper() {} /** * Transform a {@link User} into an {@link UserModel}. * * @param user Object to be transformed. * @return {@link UserModel}. */ public UserModel transform(User user) { if (user == null) { throw new IllegalArgumentException("Cannot transform a null value"); } final UserModel userModel = new UserModel(user.getUserId()); userModel.setCoverUrl(user.getCoverUrl()); userModel.setFullName(user.getFullName()); userModel.setEmail(user.getEmail()); userModel.setDescription(user.getDescription()); userModel.setFollowers(user.getFollowers()); return userModel; } /** * Transform a Collection of {@link User} into a Collection of {@link UserModel}. * * @param usersCollection Objects to be transformed. * @return List of {@link UserModel}. */ public Collection transform(Collection usersCollection) { Collection userModelsCollection; if (usersCollection != null && !usersCollection.isEmpty()) { userModelsCollection = new ArrayList<>(); for (User user : usersCollection) { userModelsCollection.add(transform(user)); } } else { userModelsCollection = Collections.emptyList(); } return userModelsCollection; } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/model/UserModel.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.model; /** * Class that represents a user in the presentation layer. */ public class UserModel { private final int userId; public UserModel(int userId) { this.userId = userId; } private String coverUrl; private String fullName; private String email; private String description; private int followers; public int getUserId() { return userId; } public String getCoverUrl() { return coverUrl; } public void setCoverUrl(String coverUrl) { this.coverUrl = coverUrl; } public String getFullName() { return fullName; } public void setFullName(String fullName) { this.fullName = fullName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public int getFollowers() { return followers; } public void setFollowers(int followers) { this.followers = followers; } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/navigation/Navigator.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.navigation; import android.content.Context; import android.content.Intent; import com.fernandocejas.android10.sample.presentation.view.activity.UserDetailsActivity; import com.fernandocejas.android10.sample.presentation.view.activity.UserListActivity; import javax.inject.Inject; import javax.inject.Singleton; /** * Class used to navigate through the application. */ @Singleton public class Navigator { @Inject public Navigator() { //empty } /** * Goes to the user list screen. * * @param context A Context needed to open the destiny activity. */ public void navigateToUserList(Context context) { if (context != null) { Intent intentToLaunch = UserListActivity.getCallingIntent(context); context.startActivity(intentToLaunch); } } /** * Goes to the user details screen. * * @param context A Context needed to open the destiny activity. */ public void navigateToUserDetails(Context context, int userId) { if (context != null) { Intent intentToLaunch = UserDetailsActivity.getCallingIntent(context, userId); context.startActivity(intentToLaunch); } } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/presenter/Presenter.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.presenter; /** * Interface representing a Presenter in a model view presenter (MVP) pattern. */ public interface Presenter { /** * Method that control the lifecycle of the view. It should be called in the view's * (Activity or Fragment) onResume() method. */ void resume(); /** * Method that control the lifecycle of the view. It should be called in the view's * (Activity or Fragment) onPause() method. */ void pause(); /** * Method that control the lifecycle of the view. It should be called in the view's * (Activity or Fragment) onDestroy() method. */ void destroy(); } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/presenter/UserDetailsPresenter.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.presenter; import android.support.annotation.NonNull; import com.fernandocejas.android10.sample.domain.User; import com.fernandocejas.android10.sample.domain.exception.DefaultErrorBundle; import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; import com.fernandocejas.android10.sample.domain.interactor.DefaultObserver; import com.fernandocejas.android10.sample.domain.interactor.GetUserDetails; import com.fernandocejas.android10.sample.domain.interactor.GetUserDetails.Params; import com.fernandocejas.android10.sample.presentation.exception.ErrorMessageFactory; import com.fernandocejas.android10.sample.presentation.internal.di.PerActivity; import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; import com.fernandocejas.android10.sample.presentation.model.UserModel; import com.fernandocejas.android10.sample.presentation.view.UserDetailsView; import javax.inject.Inject; /** * {@link Presenter} that controls communication between views and models of the presentation * layer. */ @PerActivity public class UserDetailsPresenter implements Presenter { private UserDetailsView viewDetailsView; private final GetUserDetails getUserDetailsUseCase; private final UserModelDataMapper userModelDataMapper; @Inject public UserDetailsPresenter(GetUserDetails getUserDetailsUseCase, UserModelDataMapper userModelDataMapper) { this.getUserDetailsUseCase = getUserDetailsUseCase; this.userModelDataMapper = userModelDataMapper; } public void setView(@NonNull UserDetailsView view) { this.viewDetailsView = view; } @Override public void resume() {} @Override public void pause() {} @Override public void destroy() { this.getUserDetailsUseCase.dispose(); this.viewDetailsView = null; } /** * Initializes the presenter by showing/hiding proper views * and retrieving user details. */ public void initialize(int userId) { this.hideViewRetry(); this.showViewLoading(); this.getUserDetails(userId); } private void getUserDetails(int userId) { this.getUserDetailsUseCase.execute(new UserDetailsObserver(), Params.forUser(userId)); } private void showViewLoading() { this.viewDetailsView.showLoading(); } private void hideViewLoading() { this.viewDetailsView.hideLoading(); } private void showViewRetry() { this.viewDetailsView.showRetry(); } private void hideViewRetry() { this.viewDetailsView.hideRetry(); } private void showErrorMessage(ErrorBundle errorBundle) { String errorMessage = ErrorMessageFactory.create(this.viewDetailsView.context(), errorBundle.getException()); this.viewDetailsView.showError(errorMessage); } private void showUserDetailsInView(User user) { final UserModel userModel = this.userModelDataMapper.transform(user); this.viewDetailsView.renderUser(userModel); } private final class UserDetailsObserver extends DefaultObserver { @Override public void onComplete() { UserDetailsPresenter.this.hideViewLoading(); } @Override public void onError(Throwable e) { UserDetailsPresenter.this.hideViewLoading(); UserDetailsPresenter.this.showErrorMessage(new DefaultErrorBundle((Exception) e)); UserDetailsPresenter.this.showViewRetry(); } @Override public void onNext(User user) { UserDetailsPresenter.this.showUserDetailsInView(user); } } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/presenter/UserListPresenter.java ================================================ /** * Copyright (C) 2015 Fernando Cejas Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fernandocejas.android10.sample.presentation.presenter; import android.support.annotation.NonNull; import com.fernandocejas.android10.sample.domain.User; import com.fernandocejas.android10.sample.domain.exception.DefaultErrorBundle; import com.fernandocejas.android10.sample.domain.exception.ErrorBundle; import com.fernandocejas.android10.sample.domain.interactor.DefaultObserver; import com.fernandocejas.android10.sample.domain.interactor.GetUserList; import com.fernandocejas.android10.sample.presentation.exception.ErrorMessageFactory; import com.fernandocejas.android10.sample.presentation.internal.di.PerActivity; import com.fernandocejas.android10.sample.presentation.mapper.UserModelDataMapper; import com.fernandocejas.android10.sample.presentation.model.UserModel; import com.fernandocejas.android10.sample.presentation.view.UserListView; import java.util.Collection; import java.util.List; import javax.inject.Inject; /** * {@link Presenter} that controls communication between views and models of the presentation * layer. */ @PerActivity public class UserListPresenter implements Presenter { private UserListView viewListView; private final GetUserList getUserListUseCase; private final UserModelDataMapper userModelDataMapper; @Inject public UserListPresenter(GetUserList getUserListUserCase, UserModelDataMapper userModelDataMapper) { this.getUserListUseCase = getUserListUserCase; this.userModelDataMapper = userModelDataMapper; } public void setView(@NonNull UserListView view) { this.viewListView = view; } @Override public void resume() {} @Override public void pause() {} @Override public void destroy() { this.getUserListUseCase.dispose(); this.viewListView = null; } /** * Initializes the presenter by start retrieving the user list. */ public void initialize() { this.loadUserList(); } /** * Loads all users. */ private void loadUserList() { this.hideViewRetry(); this.showViewLoading(); this.getUserList(); } public void onUserClicked(UserModel userModel) { this.viewListView.viewUser(userModel); } private void showViewLoading() { this.viewListView.showLoading(); } private void hideViewLoading() { this.viewListView.hideLoading(); } private void showViewRetry() { this.viewListView.showRetry(); } private void hideViewRetry() { this.viewListView.hideRetry(); } private void showErrorMessage(ErrorBundle errorBundle) { String errorMessage = ErrorMessageFactory.create(this.viewListView.context(), errorBundle.getException()); this.viewListView.showError(errorMessage); } private void showUsersCollectionInView(Collection usersCollection) { final Collection userModelsCollection = this.userModelDataMapper.transform(usersCollection); this.viewListView.renderUserList(userModelsCollection); } private void getUserList() { this.getUserListUseCase.execute(new UserListObserver(), null); } private final class UserListObserver extends DefaultObserver> { @Override public void onComplete() { UserListPresenter.this.hideViewLoading(); } @Override public void onError(Throwable e) { UserListPresenter.this.hideViewLoading(); UserListPresenter.this.showErrorMessage(new DefaultErrorBundle((Exception) e)); UserListPresenter.this.showViewRetry(); } @Override public void onNext(List users) { UserListPresenter.this.showUsersCollectionInView(users); } } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/LoadDataView.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view; import android.content.Context; /** * Interface representing a View that will use to load data. */ public interface LoadDataView { /** * Show a view with a progress bar indicating a loading process. */ void showLoading(); /** * Hide a loading view. */ void hideLoading(); /** * Show a retry view in case of an error when retrieving data. */ void showRetry(); /** * Hide a retry view shown if there was an error when retrieving data. */ void hideRetry(); /** * Show an error message * * @param message A string representing an error. */ void showError(String message); /** * Get a {@link android.content.Context}. */ Context context(); } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/UserDetailsView.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view; import com.fernandocejas.android10.sample.presentation.model.UserModel; /** * Interface representing a View in a model view presenter (MVP) pattern. * In this case is used as a view representing a user profile. */ public interface UserDetailsView extends LoadDataView { /** * Render a user in the UI. * * @param user The {@link UserModel} that will be shown. */ void renderUser(UserModel user); } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/UserListView.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view; import com.fernandocejas.android10.sample.presentation.model.UserModel; import java.util.Collection; /** * Interface representing a View in a model view presenter (MVP) pattern. * In this case is used as a view representing a list of {@link UserModel}. */ public interface UserListView extends LoadDataView { /** * Render a user list in the UI. * * @param userModelCollection The collection of {@link UserModel} that will be shown. */ void renderUserList(Collection userModelCollection); /** * View a {@link UserModel} profile/details. * * @param userModel The user that will be shown. */ void viewUser(UserModel userModel); } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/activity/BaseActivity.java ================================================ package com.fernandocejas.android10.sample.presentation.view.activity; import android.app.Activity; import android.app.Fragment; import android.app.FragmentTransaction; import android.os.Bundle; import com.fernandocejas.android10.sample.presentation.AndroidApplication; import com.fernandocejas.android10.sample.presentation.internal.di.components.ApplicationComponent; import com.fernandocejas.android10.sample.presentation.internal.di.modules.ActivityModule; import com.fernandocejas.android10.sample.presentation.navigation.Navigator; import javax.inject.Inject; /** * Base {@link android.app.Activity} class for every Activity in this application. */ public abstract class BaseActivity extends Activity { @Inject Navigator navigator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.getApplicationComponent().inject(this); } /** * Adds a {@link Fragment} to this activity's layout. * * @param containerViewId The container view to where add the fragment. * @param fragment The fragment to be added. */ protected void addFragment(int containerViewId, Fragment fragment) { final FragmentTransaction fragmentTransaction = this.getFragmentManager().beginTransaction(); fragmentTransaction.add(containerViewId, fragment); fragmentTransaction.commit(); } /** * Get the Main Application component for dependency injection. * * @return {@link com.fernandocejas.android10.sample.presentation.internal.di.components.ApplicationComponent} */ protected ApplicationComponent getApplicationComponent() { return ((AndroidApplication) getApplication()).getApplicationComponent(); } /** * Get an Activity module for dependency injection. * * @return {@link com.fernandocejas.android10.sample.presentation.internal.di.modules.ActivityModule} */ protected ActivityModule getActivityModule() { return new ActivityModule(this); } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/activity/MainActivity.java ================================================ package com.fernandocejas.android10.sample.presentation.view.activity; import android.os.Bundle; import android.widget.Button; import butterknife.Bind; import butterknife.ButterKnife; import butterknife.OnClick; import com.fernandocejas.android10.sample.presentation.R; /** * Main application screen. This is the app entry point. */ public class MainActivity extends BaseActivity { @Bind(R.id.btn_LoadData) Button btn_LoadData; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); } /** * Goes to the user list screen. */ @OnClick(R.id.btn_LoadData) void navigateToUserList() { this.navigator.navigateToUserList(this); } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/activity/UserDetailsActivity.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view.activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.Window; import com.fernandocejas.android10.sample.presentation.R; import com.fernandocejas.android10.sample.presentation.internal.di.HasComponent; import com.fernandocejas.android10.sample.presentation.internal.di.components.DaggerUserComponent; import com.fernandocejas.android10.sample.presentation.internal.di.components.UserComponent; import com.fernandocejas.android10.sample.presentation.view.fragment.UserDetailsFragment; /** * Activity that shows details of a certain user. */ public class UserDetailsActivity extends BaseActivity implements HasComponent { private static final String INTENT_EXTRA_PARAM_USER_ID = "org.android10.INTENT_PARAM_USER_ID"; private static final String INSTANCE_STATE_PARAM_USER_ID = "org.android10.STATE_PARAM_USER_ID"; public static Intent getCallingIntent(Context context, int userId) { Intent callingIntent = new Intent(context, UserDetailsActivity.class); callingIntent.putExtra(INTENT_EXTRA_PARAM_USER_ID, userId); return callingIntent; } private int userId; private UserComponent userComponent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setContentView(R.layout.activity_layout); this.initializeActivity(savedInstanceState); this.initializeInjector(); } @Override protected void onSaveInstanceState(Bundle outState) { if (outState != null) { outState.putInt(INSTANCE_STATE_PARAM_USER_ID, this.userId); } super.onSaveInstanceState(outState); } /** * Initializes this activity. */ private void initializeActivity(Bundle savedInstanceState) { if (savedInstanceState == null) { this.userId = getIntent().getIntExtra(INTENT_EXTRA_PARAM_USER_ID, -1); addFragment(R.id.fragmentContainer, UserDetailsFragment.forUser(userId)); } else { this.userId = savedInstanceState.getInt(INSTANCE_STATE_PARAM_USER_ID); } } private void initializeInjector() { this.userComponent = DaggerUserComponent.builder() .applicationComponent(getApplicationComponent()) .activityModule(getActivityModule()) .build(); } @Override public UserComponent getComponent() { return userComponent; } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/activity/UserListActivity.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view.activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.Window; import com.fernandocejas.android10.sample.presentation.R; import com.fernandocejas.android10.sample.presentation.internal.di.HasComponent; import com.fernandocejas.android10.sample.presentation.internal.di.components.DaggerUserComponent; import com.fernandocejas.android10.sample.presentation.internal.di.components.UserComponent; import com.fernandocejas.android10.sample.presentation.model.UserModel; import com.fernandocejas.android10.sample.presentation.view.fragment.UserListFragment; /** * Activity that shows a list of Users. */ public class UserListActivity extends BaseActivity implements HasComponent, UserListFragment.UserListListener { public static Intent getCallingIntent(Context context) { return new Intent(context, UserListActivity.class); } private UserComponent userComponent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setContentView(R.layout.activity_layout); this.initializeInjector(); if (savedInstanceState == null) { addFragment(R.id.fragmentContainer, new UserListFragment()); } } private void initializeInjector() { this.userComponent = DaggerUserComponent.builder() .applicationComponent(getApplicationComponent()) .activityModule(getActivityModule()) .build(); } @Override public UserComponent getComponent() { return userComponent; } @Override public void onUserClicked(UserModel userModel) { this.navigator.navigateToUserDetails(this, userModel.getUserId()); } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/adapter/UsersAdapter.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view.adapter; import android.content.Context; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import butterknife.Bind; import butterknife.ButterKnife; import com.fernandocejas.android10.sample.presentation.R; import com.fernandocejas.android10.sample.presentation.model.UserModel; import java.util.Collection; import java.util.Collections; import java.util.List; import javax.inject.Inject; /** * Adaptar that manages a collection of {@link UserModel}. */ public class UsersAdapter extends RecyclerView.Adapter { public interface OnItemClickListener { void onUserItemClicked(UserModel userModel); } private List usersCollection; private final LayoutInflater layoutInflater; private OnItemClickListener onItemClickListener; @Inject UsersAdapter(Context context) { this.layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.usersCollection = Collections.emptyList(); } @Override public int getItemCount() { return (this.usersCollection != null) ? this.usersCollection.size() : 0; } @Override public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = this.layoutInflater.inflate(R.layout.row_user, parent, false); return new UserViewHolder(view); } @Override public void onBindViewHolder(UserViewHolder holder, final int position) { final UserModel userModel = this.usersCollection.get(position); holder.textViewTitle.setText(userModel.getFullName()); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (UsersAdapter.this.onItemClickListener != null) { UsersAdapter.this.onItemClickListener.onUserItemClicked(userModel); } } }); } @Override public long getItemId(int position) { return position; } public void setUsersCollection(Collection usersCollection) { this.validateUsersCollection(usersCollection); this.usersCollection = (List) usersCollection; this.notifyDataSetChanged(); } public void setOnItemClickListener (OnItemClickListener onItemClickListener) { this.onItemClickListener = onItemClickListener; } private void validateUsersCollection(Collection usersCollection) { if (usersCollection == null) { throw new IllegalArgumentException("The list cannot be null"); } } static class UserViewHolder extends RecyclerView.ViewHolder { @Bind(R.id.title) TextView textViewTitle; UserViewHolder(View itemView) { super(itemView); ButterKnife.bind(this, itemView); } } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/adapter/UsersLayoutManager.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view.adapter; import android.content.Context; import android.support.v7.widget.LinearLayoutManager; /** * Layout manager to position items inside a {@link android.support.v7.widget.RecyclerView}. */ public class UsersLayoutManager extends LinearLayoutManager { public UsersLayoutManager(Context context) { super(context); } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/component/AutoLoadImageView.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view.component; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.widget.ImageView; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.net.URLConnection; /** * Simple implementation of {@link android.widget.ImageView} with extended features like setting an * image from an url and an internal file cache using the application cache directory. */ public class AutoLoadImageView extends ImageView { private static final String BASE_IMAGE_NAME_CACHED = "image_"; private String imageUrl = null; private int imagePlaceHolderResId = -1; private DiskCache cache = new DiskCache(getContext().getCacheDir()); public AutoLoadImageView(Context context) { super(context); } public AutoLoadImageView(Context context, AttributeSet attrs) { super(context, attrs); } public AutoLoadImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState savedState = new SavedState(superState); savedState.imagePlaceHolderResId = this.imagePlaceHolderResId; savedState.imageUrl = this.imageUrl; return savedState; } @Override protected void onRestoreInstanceState(Parcelable state) { if(!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState savedState = (SavedState)state; super.onRestoreInstanceState(savedState.getSuperState()); this.imagePlaceHolderResId = savedState.imagePlaceHolderResId; this.imageUrl = savedState.imageUrl; this.setImageUrl(this.imageUrl); } /** * Set an image from a remote url. * * @param imageUrl The url of the resource to load. */ public void setImageUrl(final String imageUrl) { this.imageUrl = imageUrl; AutoLoadImageView.this.loadImagePlaceHolder(); if (this.imageUrl != null) { this.loadImageFromUrl(this.imageUrl); } else { this.loadImagePlaceHolder(); } } /** * Loads and image from the internet (and cache it) or from the internal cache. * * @param imageUrl The remote image url to load. */ private void loadImageFromUrl(final String imageUrl) { new Thread() { @Override public void run() { final Bitmap bitmap = AutoLoadImageView.this.getFromCache(getFileNameFromUrl(imageUrl)); if (bitmap != null) { AutoLoadImageView.this.loadBitmap(bitmap); } else { if (isThereInternetConnection()) { final ImageDownloader imageDownloader = new ImageDownloader(); imageDownloader.download(imageUrl, new ImageDownloader.Callback() { @Override public void onImageDownloaded(Bitmap bitmap) { AutoLoadImageView.this.cacheBitmap(bitmap, getFileNameFromUrl(imageUrl)); AutoLoadImageView.this.loadBitmap(bitmap); } @Override public void onError() { AutoLoadImageView.this.loadImagePlaceHolder(); } }); } else { AutoLoadImageView.this.loadImagePlaceHolder(); } } } }.start(); } /** * Run the operation of loading a bitmap on the UI thread. * * @param bitmap The image to load. */ private void loadBitmap(final Bitmap bitmap) { ((Activity) getContext()).runOnUiThread(new Runnable() { @Override public void run() { AutoLoadImageView.this.setImageBitmap(bitmap); } }); } /** * Loads the image place holder if any has been assigned. */ private void loadImagePlaceHolder() { if (this.imagePlaceHolderResId != -1) { ((Activity) getContext()).runOnUiThread(new Runnable() { @Override public void run() { AutoLoadImageView.this.setImageResource( AutoLoadImageView.this.imagePlaceHolderResId); } }); } } /** * Get a {@link android.graphics.Bitmap} from the internal cache or null if it does not exist. * * @param fileName The name of the file to look for in the cache. * @return A valid cached bitmap, otherwise null. */ private Bitmap getFromCache(String fileName) { Bitmap bitmap = null; if (this.cache != null) { bitmap = this.cache.get(fileName); } return bitmap; } /** * Cache an image using the internal cache. * * @param bitmap The bitmap to cache. * @param fileName The file name used for caching the bitmap. */ private void cacheBitmap(Bitmap bitmap, String fileName) { if (this.cache != null) { this.cache.put(bitmap, fileName); } } /** * Checks if the device has any active internet connection. * * @return true device with internet connection, otherwise false. */ private boolean isThereInternetConnection() { boolean isConnected; final ConnectivityManager connectivityManager = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); isConnected = (networkInfo != null && networkInfo.isConnectedOrConnecting()); return isConnected; } /** * Creates a file name from an image url * * @param imageUrl The image url used to build the file name. * @return An String representing a unique file name. */ private String getFileNameFromUrl(String imageUrl) { //we could generate an unique MD5/SHA-1 here String hash = String.valueOf(imageUrl.hashCode()); if (hash.startsWith("-")) { hash = hash.substring(1); } return BASE_IMAGE_NAME_CACHED + hash; } /** * Class used to download images from the internet */ private static class ImageDownloader { interface Callback { void onImageDownloaded(Bitmap bitmap); void onError(); } ImageDownloader() {} /** * Download an image from an url. * * @param imageUrl The url of the image to download. * @param callback A callback used to be reported when the task is finished. */ void download(String imageUrl, Callback callback) { try { final URLConnection conn = new URL(imageUrl).openConnection(); conn.connect(); final Bitmap bitmap = BitmapFactory.decodeStream(conn.getInputStream()); if (callback != null) { callback.onImageDownloaded(bitmap); } } catch (IOException e) { reportError(callback); } } /** * Report an error to the caller * * @param callback Caller implementing {@link Callback} */ private void reportError(Callback callback) { if (callback != null) { callback.onError(); } } } /** * A simple disk cache implementation */ private static class DiskCache { private static final String TAG = "DiskCache"; private final File cacheDir; DiskCache(File cacheDir) { this.cacheDir = cacheDir; } /** * Get an element from the cache. * * @param fileName The name of the file to look for. * @return A valid element, otherwise false. */ synchronized Bitmap get(String fileName) { Bitmap bitmap = null; File file = buildFileFromFilename(fileName); if (file.exists()) { bitmap = BitmapFactory.decodeFile(file.getPath()); } return bitmap; } /** * Cache an element. * * @param bitmap The bitmap to be put in the cache. * @param fileName A string representing the name of the file to be cached. */ synchronized void put(Bitmap bitmap, String fileName) { final File file = buildFileFromFilename(fileName); if (!file.exists()) { try { final FileOutputStream fileOutputStream = new FileOutputStream(file); bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream); fileOutputStream.flush(); fileOutputStream.close(); } catch (IOException e) { Log.e(TAG, e.getMessage()); } } } /** * Creates a file name from an image url * * @param fileName The image url used to build the file name. * @return A {@link java.io.File} representing a unique element. */ private File buildFileFromFilename(String fileName) { String fullPath = this.cacheDir.getPath() + File.separator + fileName; return new File(fullPath); } } private static class SavedState extends BaseSavedState { int imagePlaceHolderResId; String imageUrl; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); this.imagePlaceHolderResId = in.readInt(); this.imageUrl = in.readString(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(this.imagePlaceHolderResId); out.writeString(this.imageUrl); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/fragment/BaseFragment.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view.fragment; import android.app.Fragment; import android.widget.Toast; import com.fernandocejas.android10.sample.presentation.internal.di.HasComponent; /** * Base {@link android.app.Fragment} class for every fragment in this application. */ public abstract class BaseFragment extends Fragment { /** * Shows a {@link android.widget.Toast} message. * * @param message An string representing a message to be shown. */ protected void showToastMessage(String message) { Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT).show(); } /** * Gets a component for dependency injection by its type. */ @SuppressWarnings("unchecked") protected C getComponent(Class componentType) { return componentType.cast(((HasComponent) getActivity()).getComponent()); } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/fragment/UserDetailsFragment.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view.fragment; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.RelativeLayout; import android.widget.TextView; import butterknife.Bind; import butterknife.ButterKnife; import butterknife.OnClick; import com.fernandocejas.android10.sample.presentation.R; import com.fernandocejas.android10.sample.presentation.internal.di.components.UserComponent; import com.fernandocejas.android10.sample.presentation.model.UserModel; import com.fernandocejas.android10.sample.presentation.presenter.UserDetailsPresenter; import com.fernandocejas.android10.sample.presentation.view.UserDetailsView; import com.fernandocejas.android10.sample.presentation.view.component.AutoLoadImageView; import com.fernandocejas.arrow.checks.Preconditions; import javax.inject.Inject; /** * Fragment that shows details of a certain user. */ public class UserDetailsFragment extends BaseFragment implements UserDetailsView { private static final String PARAM_USER_ID = "param_user_id"; @Inject UserDetailsPresenter userDetailsPresenter; @Bind(R.id.iv_cover) AutoLoadImageView iv_cover; @Bind(R.id.tv_fullname) TextView tv_fullname; @Bind(R.id.tv_email) TextView tv_email; @Bind(R.id.tv_followers) TextView tv_followers; @Bind(R.id.tv_description) TextView tv_description; @Bind(R.id.rl_progress) RelativeLayout rl_progress; @Bind(R.id.rl_retry) RelativeLayout rl_retry; @Bind(R.id.bt_retry) Button bt_retry; public static UserDetailsFragment forUser(int userId) { final UserDetailsFragment userDetailsFragment = new UserDetailsFragment(); final Bundle arguments = new Bundle(); arguments.putInt(PARAM_USER_ID, userId); userDetailsFragment.setArguments(arguments); return userDetailsFragment; } public UserDetailsFragment() { setRetainInstance(true); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.getComponent(UserComponent.class).inject(this); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View fragmentView = inflater.inflate(R.layout.fragment_user_details, container, false); ButterKnife.bind(this, fragmentView); return fragmentView; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); this.userDetailsPresenter.setView(this); if (savedInstanceState == null) { this.loadUserDetails(); } } @Override public void onResume() { super.onResume(); this.userDetailsPresenter.resume(); } @Override public void onPause() { super.onPause(); this.userDetailsPresenter.pause(); } @Override public void onDestroyView() { super.onDestroyView(); ButterKnife.unbind(this); } @Override public void onDestroy() { super.onDestroy(); this.userDetailsPresenter.destroy(); } @Override public void renderUser(UserModel user) { if (user != null) { this.iv_cover.setImageUrl(user.getCoverUrl()); this.tv_fullname.setText(user.getFullName()); this.tv_email.setText(user.getEmail()); this.tv_followers.setText(String.valueOf(user.getFollowers())); this.tv_description.setText(user.getDescription()); } } @Override public void showLoading() { this.rl_progress.setVisibility(View.VISIBLE); this.getActivity().setProgressBarIndeterminateVisibility(true); } @Override public void hideLoading() { this.rl_progress.setVisibility(View.GONE); this.getActivity().setProgressBarIndeterminateVisibility(false); } @Override public void showRetry() { this.rl_retry.setVisibility(View.VISIBLE); } @Override public void hideRetry() { this.rl_retry.setVisibility(View.GONE); } @Override public void showError(String message) { this.showToastMessage(message); } @Override public Context context() { return getActivity().getApplicationContext(); } /** * Load user details. */ private void loadUserDetails() { if (this.userDetailsPresenter != null) { this.userDetailsPresenter.initialize(currentUserId()); } } /** * Get current user id from fragments arguments. */ private int currentUserId() { final Bundle arguments = getArguments(); Preconditions.checkNotNull(arguments, "Fragment arguments cannot be null"); return arguments.getInt(PARAM_USER_ID); } @OnClick(R.id.bt_retry) void onButtonRetryClick() { UserDetailsFragment.this.loadUserDetails(); } } ================================================ FILE: presentation/src/main/java/com/fernandocejas/android10/sample/presentation/view/fragment/UserListFragment.java ================================================ /** * Copyright (C) 2014 android10.org. All rights reserved. * * @author Fernando Cejas (the android10 coder) */ package com.fernandocejas.android10.sample.presentation.view.fragment; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.RelativeLayout; import butterknife.Bind; import butterknife.ButterKnife; import butterknife.OnClick; import com.fernandocejas.android10.sample.presentation.R; import com.fernandocejas.android10.sample.presentation.internal.di.components.UserComponent; import com.fernandocejas.android10.sample.presentation.model.UserModel; import com.fernandocejas.android10.sample.presentation.presenter.UserListPresenter; import com.fernandocejas.android10.sample.presentation.view.UserListView; import com.fernandocejas.android10.sample.presentation.view.adapter.UsersAdapter; import com.fernandocejas.android10.sample.presentation.view.adapter.UsersLayoutManager; import java.util.Collection; import javax.inject.Inject; /** * Fragment that shows a list of Users. */ public class UserListFragment extends BaseFragment implements UserListView { /** * Interface for listening user list events. */ public interface UserListListener { void onUserClicked(final UserModel userModel); } @Inject UserListPresenter userListPresenter; @Inject UsersAdapter usersAdapter; @Bind(R.id.rv_users) RecyclerView rv_users; @Bind(R.id.rl_progress) RelativeLayout rl_progress; @Bind(R.id.rl_retry) RelativeLayout rl_retry; @Bind(R.id.bt_retry) Button bt_retry; private UserListListener userListListener; public UserListFragment() { setRetainInstance(true); } @Override public void onAttach(Activity activity) { super.onAttach(activity); if (activity instanceof UserListListener) { this.userListListener = (UserListListener) activity; } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.getComponent(UserComponent.class).inject(this); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View fragmentView = inflater.inflate(R.layout.fragment_user_list, container, false); ButterKnife.bind(this, fragmentView); setupRecyclerView(); return fragmentView; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); this.userListPresenter.setView(this); if (savedInstanceState == null) { this.loadUserList(); } } @Override public void onResume() { super.onResume(); this.userListPresenter.resume(); } @Override public void onPause() { super.onPause(); this.userListPresenter.pause(); } @Override public void onDestroyView() { super.onDestroyView(); rv_users.setAdapter(null); ButterKnife.unbind(this); } @Override public void onDestroy() { super.onDestroy(); this.userListPresenter.destroy(); } @Override public void onDetach() { super.onDetach(); this.userListListener = null; } @Override public void showLoading() { this.rl_progress.setVisibility(View.VISIBLE); this.getActivity().setProgressBarIndeterminateVisibility(true); } @Override public void hideLoading() { this.rl_progress.setVisibility(View.GONE); this.getActivity().setProgressBarIndeterminateVisibility(false); } @Override public void showRetry() { this.rl_retry.setVisibility(View.VISIBLE); } @Override public void hideRetry() { this.rl_retry.setVisibility(View.GONE); } @Override public void renderUserList(Collection userModelCollection) { if (userModelCollection != null) { this.usersAdapter.setUsersCollection(userModelCollection); } } @Override public void viewUser(UserModel userModel) { if (this.userListListener != null) { this.userListListener.onUserClicked(userModel); } } @Override public void showError(String message) { this.showToastMessage(message); } @Override public Context context() { return this.getActivity().getApplicationContext(); } private void setupRecyclerView() { this.usersAdapter.setOnItemClickListener(onItemClickListener); this.rv_users.setLayoutManager(new UsersLayoutManager(context())); this.rv_users.setAdapter(usersAdapter); } /** * Loads all users. */ private void loadUserList() { this.userListPresenter.initialize(); } @OnClick(R.id.bt_retry) void onButtonRetryClick() { UserListFragment.this.loadUserList(); } private UsersAdapter.OnItemClickListener onItemClickListener = new UsersAdapter.OnItemClickListener() { @Override public void onUserItemClicked(UserModel userModel) { if (UserListFragment.this.userListPresenter != null && userModel != null) { UserListFragment.this.userListPresenter.onUserClicked(userModel); } } }; } ================================================ FILE: presentation/src/main/res/drawable/selector_item_user.xml ================================================ ================================================ FILE: presentation/src/main/res/layout/activity_layout.xml ================================================ ================================================ FILE: presentation/src/main/res/layout/activity_main.xml ================================================