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
-----------------

Architectural approach
-----------------

Architectural reactive approach
-----------------

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.

[](https://android-arsenal.com/details/3/909)
================================================
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