├── Demo-JokesApp.gif
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── prieto
│ │ └── fernando
│ │ └── jokesapp
│ │ ├── custom
│ │ ├── CustomFragmentRobot.kt
│ │ └── CustomFragmentTest.kt
│ │ ├── dashboard
│ │ ├── DashboardFragmentRobot.kt
│ │ └── DashboardFragmentTest.kt
│ │ ├── detail
│ │ ├── DetailFragmentRobot.kt
│ │ └── DetailFragmentTest.kt
│ │ ├── infinite
│ │ ├── InfiniteFragmentRobot.kt
│ │ └── InfiniteFragmentTest.kt
│ │ ├── utils
│ │ ├── RecyclerViewItemCountAssertion.kt
│ │ ├── RecyclerViewMatcher.kt
│ │ └── TestConfigurationRule.kt
│ │ └── webmock
│ │ ├── AssetReaderUtil.kt
│ │ ├── ErrorDispatcher.kt
│ │ ├── MockTestRunner.kt
│ │ ├── SuccessDispatcher.kt
│ │ └── TestConfigurationBuilder.kt
│ ├── debug
│ └── assets
│ │ └── network_files
│ │ ├── custom_joke_success.json
│ │ ├── joke_list_success.json
│ │ └── random_joke_success.json
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-web.png
│ ├── java
│ └── prieto
│ │ └── fernando
│ │ └── jokesapp
│ │ ├── JokesApp.kt
│ │ ├── di
│ │ ├── ActivityScope.kt
│ │ ├── AppComponent.kt
│ │ ├── AppModule.kt
│ │ └── MainActivityModule.kt
│ │ └── view
│ │ ├── MainActivity.kt
│ │ ├── custom
│ │ └── CustomJokeFragment.kt
│ │ ├── dashboard
│ │ └── DashboardFragment.kt
│ │ ├── detail
│ │ ├── DetailFragment.kt
│ │ └── DetailViewModel.kt
│ │ ├── extension
│ │ └── Lifecycle.kt
│ │ └── infinite
│ │ ├── InfiniteJokesFragment.kt
│ │ ├── adapter
│ │ └── JokesAdapter.kt
│ │ └── widget
│ │ └── InfiniteScrollListener.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ ├── ic_launcher_background.xml
│ └── ic_launcher_foreground.xml
│ ├── layout
│ ├── activity_main.xml
│ ├── fragment_custom_joke.xml
│ ├── fragment_dashboard.xml
│ ├── fragment_detail.xml
│ ├── fragment_infinite_jokes.xml
│ └── item_joke.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ └── styles.xml
├── base-android-library.gradle
├── build.gradle
├── buildsystem
└── dependencies.gradle
├── core
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── prieto
│ │ └── fernando
│ │ └── core
│ │ └── ExampleInstrumentedTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── prieto
│ │ └── fernando
│ │ ├── presentation
│ │ ├── BaseViewModel.kt
│ │ ├── SchedulerProvider.kt
│ │ └── ViewModelProviderFactory.kt
│ │ └── ui
│ │ ├── BaseActivity.kt
│ │ ├── BaseFragment.kt
│ │ └── BaseView.kt
│ └── res
│ └── values
│ └── strings.xml
├── data-cache
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── prieto
│ │ └── fernando
│ │ └── data_cache
│ │ └── ExampleInstrumentedTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── prieto
│ │ └── fernando
│ │ ├── model
│ │ └── RandomJokeLocalModel.kt
│ │ └── source
│ │ └── JokesLocalSource.kt
│ └── res
│ └── values
│ └── strings.xml
├── data-jokesapi
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── prieto
│ │ └── fernando
│ │ └── data_jokesapi
│ │ └── ExampleInstrumentedTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── prieto
│ │ └── fernando
│ │ ├── ApiService.kt
│ │ ├── data
│ │ └── JokesRemoteSource.kt
│ │ ├── di
│ │ ├── BaseUrl.kt
│ │ ├── ChuckNorrisApiModule.kt
│ │ └── NetworkModule.kt
│ │ └── model
│ │ ├── MultipleRandomJokeResponse.kt
│ │ └── RandomJokeResponse.kt
│ └── res
│ └── values
│ └── strings.xml
├── domain
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── prieto
│ │ └── fernando
│ │ └── domain
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── prieto
│ │ │ └── fernando
│ │ │ ├── data
│ │ │ └── RandomJokeDomainModel.kt
│ │ │ ├── di
│ │ │ └── RepositoryModule.kt
│ │ │ ├── mapper
│ │ │ ├── MultipleRandomJokeResponseToLocalModelMapper.kt
│ │ │ ├── RandomJokeLocalToDomainModelMapper.kt
│ │ │ └── RandomJokeResponseToLocalModelMapper.kt
│ │ │ ├── repository
│ │ │ └── JokesRepositoryImpl.kt
│ │ │ └── usecase
│ │ │ ├── GetCustomRandomJokeUseCase.kt
│ │ │ ├── GetMultipleRandomJokeUseCase.kt
│ │ │ ├── GetRandomJokeUseCase.kt
│ │ │ └── ResetCustomRandomJokeUseCase.kt
│ └── res
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── prieto
│ └── fernando
│ └── domain
│ ├── mapper
│ ├── MultipleRandomJokeResponseToLocalModelMapperTest.kt
│ ├── RandomJokeLocalToDomainModelMapperTest.kt
│ └── RandomJokeResponseToLocalModelMapperTest.kt
│ ├── repository
│ └── JokesRepositoryImplTest.kt
│ └── usecase
│ ├── GetCustomRandomJokeUseCaseTest.kt
│ ├── GetMultipleRandomJokeUseCaseTest.kt
│ └── GetRandomJokeUseCaseTest.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── navigation
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── res
│ └── navigation
│ └── nav_graph.xml
├── presentation
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── prieto
│ │ └── fernando
│ │ └── presentation
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── prieto
│ │ │ └── fernando
│ │ │ └── presentation
│ │ │ ├── RandomJokeUiModel.kt
│ │ │ ├── custom
│ │ │ ├── CustomJokeViewModel.kt
│ │ │ └── NamesButtonStateEvaluator.kt
│ │ │ ├── dashboard
│ │ │ └── DashboardViewModel.kt
│ │ │ ├── data
│ │ │ └── RandomJokeAndTitleResource.kt
│ │ │ ├── infinite
│ │ │ └── InfiniteJokesViewModel.kt
│ │ │ ├── main
│ │ │ └── MainViewModel.kt
│ │ │ ├── mapper
│ │ │ └── RandomJokeDomainToUiModelMapper.kt
│ │ │ └── model
│ │ │ └── NamesData.kt
│ └── res
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── prieto
│ └── fernando
│ └── presentation
│ ├── CustomJokeViewModelTest.kt
│ ├── NamesButtonStateEvaluatorTest.kt
│ ├── ViewModelSetup.kt
│ ├── dashboard
│ └── DashboardViewModelTest.kt
│ ├── infinite
│ └── InfiniteJokesViewModelTest.kt
│ ├── mapper
│ └── RandomJokeDomainToUiModelMapperTest.kt
│ └── scheduler
│ └── TestSchedulerProvider.kt
└── settings.gradle
/Demo-JokesApp.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/Demo-JokesApp.gif
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JokesApp
2 |
3 | A demo app to show random jokes, with the aim of showing Clean Architecture and Clean code principles
4 | in a MVVM setup, LiveData and Rxjava.
5 |
6 | There's a cache memory used to save and retrieve a custom joke. In order to show a data cache implementation.
7 |
8 | ## About the project
9 |
10 | For simplicity, I've chosen the ICNDb free API:
11 | http://www.icndb.com/api/
12 |
13 | ### Initial approach
14 |
15 | The initial Clean Architecture approach was developed by me in a Bitbucket repository using:
16 | - MVVM
17 | - RXJava
18 |
19 | ### Goals
20 |
21 | In order to follow the latest Android standards, the project will include the next points (progressively):
22 | - ~~Complete the VM unit tests~~ (DONE)
23 | - ~~LiveData, instead of Rx (outputs)~~ (DONE)
24 | - ~~UI Tests using Espresso and Robot pattern~~ (reference [Adam McNeilly]) (DONE)
25 | - ~~MockWebServer for testing HTTP clients~~ (DONE)
26 | - ~~Add Navigation Module~~ (DONE)
27 | - ~~Improve UI elements~~ (DONE)
28 | - ~~Add feature Detail~~ (DONE)
29 | - Change modules for feature Module, in order to create two branches: `Feature-Modules` and `Data-Layer-Modules`
30 |
31 |
32 | [Adam McNeilly]: https://github.com/AdamMc331
33 |
34 | ## Libraries Used
35 |
36 | * [Rx][0] for reactive style programming (from VM to Data).
37 | * [LiveData][1] for reactive style programming (from VM to UI).
38 | * [Navigation][2] for in-app navigation.
39 | * [Rx][3] for cache storage.
40 | * [Dagger2][4] for dependency injection.
41 | * [Retrofit][5] for REST api communication.
42 | * [Timber][6] for logging.
43 | * [Espresso][7] for UI tests.
44 | * [Mockito-Kotlin][8] for mocking in tests.
45 | * [MockWebServer][9] for Instrumentation tests.
46 | * [AndroidX Test Library][10] for providing JUnit4 and functions as `launchActivity` in UI tests,
47 |
48 | [0]: https://github.com/ReactiveX/RxAndroid
49 | [1]: https://developer.android.com/topic/libraries/architecture/livedata
50 | [2]: https://developer.android.com/topic/libraries/architecture/navigation/
51 | [3]: https://github.com/ReactiveX/RxAndroid
52 | [4]: https://github.com/google/dagger
53 | [5]: https://github.com/square/retrofit
54 | [6]: https://github.com/JakeWharton/timber
55 | [7]: https://developer.android.com/training/testing/espresso/
56 | [8]: https://github.com/nhaarman/mockito-kotlin
57 | [9]: https://github.com/square/okhttp/tree/master/mockwebserver
58 | [10]: https://github.com/android/android-test
59 |
60 | ## Demo
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | # License
69 |
70 | Copyright 2019 Fernando Prieto Moyano
71 |
72 | Licensed under the Apache License, Version 2.0 (the "License");
73 | you may not use this file except in compliance with the License.
74 | You may obtain a copy of the License at
75 |
76 | http://www.apache.org/licenses/LICENSE-2.0
77 |
78 | Unless required by applicable law or agreed to in writing, software
79 | distributed under the License is distributed on an "AS IS" BASIS,
80 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
81 | See the License for the specific language governing permissions and
82 | limitations under the License.
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | apply plugin: "kotlin-kapt"
8 |
9 | apply plugin: "org.jmailen.kotlinter"
10 |
11 | apply plugin: "androidx.navigation.safeargs"
12 |
13 | android {
14 | def config = rootProject.extensions.getByName("ext")
15 |
16 | compileSdkVersion config["compile_sdk"]
17 | buildToolsVersion config["build_version"]
18 |
19 | defaultConfig {
20 | applicationId config["application_id"]
21 | minSdkVersion config["min_sdk"]
22 | targetSdkVersion config["target_sdk"]
23 | versionCode config["version_code"]
24 | versionName config["version_name"]
25 | testInstrumentationRunner config["test_runner"]
26 | multiDexEnabled true
27 | }
28 |
29 | compileOptions {
30 | sourceCompatibility 1.8
31 | targetCompatibility 1.8
32 | }
33 |
34 | kotlinOptions {
35 | jvmTarget = '1.8'
36 | }
37 |
38 | buildTypes {
39 | debug {
40 | buildConfigField("Integer",
41 | "PORT",
42 | "8080")
43 | }
44 | release {
45 | minifyEnabled false
46 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
47 | }
48 | }
49 |
50 | testOptions {
51 | unitTests.returnDefaultValues = true
52 |
53 | unitTests.all {
54 | setIgnoreFailures(false)
55 | }
56 | }
57 |
58 | lintOptions {
59 | quiet true
60 | xmlReport true
61 | htmlReport true
62 | abortOnError true
63 | warningsAsErrors false
64 | }
65 |
66 | androidExtensions {
67 | experimental true
68 | }
69 | }
70 |
71 | dependencies {
72 | def applicationDependencies = rootProject.ext.mainApplication
73 | def unitTestDependencies = rootProject.ext.unitTesting
74 | def acceptanceTestDependencies = rootProject.ext.acceptanceTesting
75 |
76 | implementation project(':core')
77 | implementation project(':data-jokesapi')
78 | implementation project(':data-cache')
79 | implementation project(':domain')
80 | implementation project(':presentation')
81 | implementation project(':navigation')
82 |
83 | compileOnly applicationDependencies.jdk9Builder
84 |
85 | // Compile time dependencies
86 | annotationProcessor applicationDependencies.lifecycleCompiler
87 | kapt applicationDependencies.daggerCompiler
88 | kapt applicationDependencies.daggerAndroidProcessor
89 |
90 | // Application dependencies
91 | implementation applicationDependencies.kotlin
92 | implementation applicationDependencies.appCompat
93 | implementation applicationDependencies.coreKtx
94 | implementation applicationDependencies.constraintLayout
95 | implementation applicationDependencies.recyclerView
96 | implementation applicationDependencies.cardView
97 | implementation applicationDependencies.archViewModel
98 | implementation applicationDependencies.archComponents
99 | implementation applicationDependencies.lifecycleCompiler
100 | implementation applicationDependencies.archComponentsCompiler
101 | implementation applicationDependencies.archNavigationFragment
102 | implementation applicationDependencies.archNavigationUi
103 | implementation applicationDependencies.daggerAndroid
104 | implementation applicationDependencies.daggerAndroidSupport
105 | implementation applicationDependencies.retrofit
106 | implementation applicationDependencies.retrofitConverterGson
107 | implementation applicationDependencies.retrofitRxjava2Adapter
108 | implementation applicationDependencies.okHttpLoggingInterceptor
109 | implementation applicationDependencies.rxJava
110 | implementation applicationDependencies.rxKotlin
111 | implementation applicationDependencies.rxAndroid
112 | implementation applicationDependencies.rxBinding
113 | implementation applicationDependencies.timber
114 |
115 | // Unit/Integration tests dependencies
116 | testImplementation unitTestDependencies.junit
117 | testImplementation unitTestDependencies.mockitoCore
118 | testImplementation unitTestDependencies.mockitoInline
119 | testImplementation unitTestDependencies.mockitoKotlin
120 | testImplementation unitTestDependencies.junitPlatformRunner
121 | testImplementation unitTestDependencies.commonsCodec
122 | testImplementation unitTestDependencies.coreTesting
123 |
124 | // Acceptance tests dependencies
125 | androidTestImplementation acceptanceTestDependencies.testEspressoContrib, {
126 | exclude group: 'com.google.code.findbugs', module: 'jsr305'
127 | }
128 | androidTestImplementation acceptanceTestDependencies.testRunner
129 | androidTestImplementation acceptanceTestDependencies.testRules
130 | androidTestImplementation acceptanceTestDependencies.testExt
131 | androidTestImplementation acceptanceTestDependencies.webMockServer
132 | androidTestImplementation acceptanceTestDependencies.testCore
133 | androidTestImplementation acceptanceTestDependencies.coreKtx
134 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/custom/CustomFragmentRobot.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.custom
2 |
3 | import androidx.test.espresso.Espresso.onView
4 | import androidx.test.espresso.action.ViewActions.click
5 | import androidx.test.espresso.action.ViewActions.typeText
6 | import androidx.test.espresso.assertion.ViewAssertions.matches
7 | import androidx.test.espresso.matcher.ViewMatchers.*
8 | import org.hamcrest.Matchers.not
9 | import prieto.fernando.jokesapp.R
10 |
11 | fun customFragmentRobot(func: CustomFragmentRobot.()-> Unit) = CustomFragmentRobot()
12 | .apply { func() }
13 |
14 | class CustomFragmentRobot {
15 |
16 | fun clickFirstNameEditTextView() = apply {
17 | onView(firstNameEditTextViewMatcher).perform(click())
18 | }
19 |
20 | fun clickLastNameEditTextView() = apply {
21 | onView(lastNameEditTextViewMatcher).perform(click())
22 | }
23 |
24 | fun inputFirstNameEditTextView(text: String) = apply {
25 | onView(firstNameEditTextViewMatcher).perform(typeText(text))
26 | }
27 |
28 | fun inputLastNameEditTextView(text: String) = apply {
29 | onView(lastNameEditTextViewMatcher).perform(typeText(text))
30 | }
31 |
32 | fun assertFirstNameEditTextViewDisplayed() = apply {
33 | onView(firstNameEditTextViewMatcher).check(matches(isDisplayed()))
34 | }
35 |
36 | fun assertLastNameEditTextViewDisplayed() = apply {
37 | onView(lastNameEditTextViewMatcher).check(matches(isDisplayed()))
38 | }
39 |
40 | fun clickDoneButton() = apply {
41 | onView(doneButtonMatcher).perform(click())
42 | }
43 |
44 | fun enabledDoneButton() = apply {
45 | onView(doneButtonMatcher).check(matches(isEnabled()))
46 | }
47 |
48 | fun disabledDoneButton() = apply {
49 | onView(doneButtonMatcher).check(matches(not(isEnabled())))
50 | }
51 |
52 | companion object {
53 | private val firstNameEditTextViewMatcher = withId(R.id.first_name_text)
54 | private val lastNameEditTextViewMatcher = withId(R.id.last_name_text)
55 | private val doneButtonMatcher = withId(R.id.button_done)
56 | }
57 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/custom/CustomFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.custom
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import okhttp3.mockwebserver.MockWebServer
5 | import org.junit.After
6 | import org.junit.Before
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import prieto.fernando.jokesapp.BuildConfig
11 | import prieto.fernando.jokesapp.dashboard.dashboardFragmentRobot
12 | import prieto.fernando.jokesapp.utils.TestConfigurationRule
13 | import prieto.fernando.jokesapp.webmock.SuccessDispatcher
14 |
15 | @RunWith(AndroidJUnit4::class)
16 | class CustomFragmentTest {
17 |
18 | @get:Rule
19 | val espressoRule = TestConfigurationRule()
20 |
21 | private val mockWebServer = MockWebServer()
22 |
23 | @Before
24 | fun setup() {
25 | mockWebServer.start(BuildConfig.PORT)
26 | }
27 |
28 | @After
29 | fun teardown() {
30 | mockWebServer.shutdown()
31 | }
32 |
33 | @Test
34 | fun textInputsNotPassingCriteria() {
35 | dashboardFragmentRobot {
36 | assertButtonCustomJokeDisplayed()
37 | clickButtonCustomJoke()
38 | }
39 |
40 | customFragmentRobot {
41 | assertFirstNameEditTextViewDisplayed()
42 | clickFirstNameEditTextView()
43 | inputFirstNameEditTextView("a.08483")
44 | assertLastNameEditTextViewDisplayed()
45 | clickLastNameEditTextView()
46 | inputLastNameEditTextView("a")
47 | disabledDoneButton()
48 | }
49 | }
50 |
51 | @Test
52 | fun textInputsPassingCriteria() {
53 | dashboardFragmentRobot {
54 | assertButtonCustomJokeDisplayed()
55 | clickButtonCustomJoke()
56 | }
57 |
58 | customFragmentRobot {
59 | assertFirstNameEditTextViewDisplayed()
60 | clickFirstNameEditTextView()
61 | inputFirstNameEditTextView("Fernando")
62 | assertLastNameEditTextViewDisplayed()
63 | clickLastNameEditTextView()
64 | inputLastNameEditTextView("Prieto")
65 | enabledDoneButton()
66 | }
67 | }
68 |
69 | @Test
70 | fun setCustomMessageAndDialogViewPrompted() {
71 | dashboardFragmentRobot {
72 | assertButtonCustomJokeDisplayed()
73 | clickButtonCustomJoke()
74 | }
75 |
76 | mockWebServer.dispatcher = SuccessDispatcher()
77 |
78 | customFragmentRobot {
79 | assertFirstNameEditTextViewDisplayed()
80 | clickFirstNameEditTextView()
81 | inputFirstNameEditTextView("Fernando")
82 | assertLastNameEditTextViewDisplayed()
83 | clickLastNameEditTextView()
84 | inputLastNameEditTextView("Prieto")
85 | enabledDoneButton()
86 | clickDoneButton()
87 | }
88 |
89 | dashboardFragmentRobot {
90 | assertDialogViewCustomJokeDisplayed()
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/dashboard/DashboardFragmentRobot.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.dashboard
2 |
3 | import androidx.test.espresso.Espresso.onView
4 | import androidx.test.espresso.action.ViewActions.click
5 | import androidx.test.espresso.assertion.ViewAssertions.matches
6 | import androidx.test.espresso.matcher.RootMatchers.isDialog
7 | import androidx.test.espresso.matcher.ViewMatchers.*
8 | import prieto.fernando.jokesapp.R
9 |
10 | fun dashboardFragmentRobot(func: DashboardFragmentRobot.() -> Unit) =
11 | DashboardFragmentRobot().apply { func() }
12 |
13 | class DashboardFragmentRobot {
14 |
15 | fun assertButtonRandomJokeDisplayed() = apply {
16 | onView(buttonRandomJokeMatcher).check(matches(isDisplayed()))
17 | }
18 |
19 | fun clickButtonRandomJoke() = apply {
20 | onView(buttonRandomJokeMatcher).perform(click())
21 | }
22 |
23 | fun assertDialogViewRandomJokeDisplayed() = apply {
24 | onView(dialogRandomTitleViewMatcher)
25 | .inRoot(isDialog())
26 | .check(matches(isDisplayed()))
27 | }
28 |
29 | fun assertDialogViewCustomJokeDisplayed() = apply {
30 | onView(dialogCustomTitleViewMatcher)
31 | .inRoot(isDialog())
32 | .check(matches(isDisplayed()))
33 | }
34 |
35 | fun clickDismissButtonDialog() = apply {
36 | onView(dialogButtonViewMatcher).perform(click())
37 | }
38 |
39 | fun assertButtonCustomJokeDisplayed() = apply {
40 | onView(buttonCustomJokeMatcher).check(matches(isDisplayed()))
41 | }
42 |
43 | fun clickButtonCustomJoke() = apply {
44 | onView(buttonCustomJokeMatcher).perform(click())
45 | }
46 |
47 | fun assertButtonInfiniteJokesDisplayed() = apply {
48 | onView(buttonInfiniteJokesMatcher).check(matches(isDisplayed()))
49 | }
50 |
51 | fun clickButtonInfiniteJokes() = apply {
52 | onView(buttonInfiniteJokesMatcher).perform(click())
53 | }
54 |
55 | companion object {
56 | private val buttonRandomJokeMatcher = withId(R.id.button_random_joke)
57 | private val dialogRandomTitleViewMatcher = withText("Random Joke")
58 | private val dialogCustomTitleViewMatcher = withText("Custom Joke")
59 | private val dialogButtonViewMatcher = withText("DISMISS")
60 | private val buttonCustomJokeMatcher = withId(R.id.button_custom_joke)
61 | private val buttonInfiniteJokesMatcher = withId(R.id.button_multiple_jokes)
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/dashboard/DashboardFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.dashboard
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import okhttp3.mockwebserver.MockWebServer
5 | import org.junit.After
6 | import org.junit.Before
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import prieto.fernando.jokesapp.BuildConfig
11 | import prieto.fernando.jokesapp.custom.customFragmentRobot
12 | import prieto.fernando.jokesapp.infinite.infiniteFragmentRobot
13 | import prieto.fernando.jokesapp.utils.TestConfigurationRule
14 | import prieto.fernando.jokesapp.webmock.SuccessDispatcher
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | class DashboardFragmentTest {
18 |
19 | @get:Rule
20 | val espressoRule = TestConfigurationRule()
21 |
22 | private val mockWebServer = MockWebServer()
23 |
24 | @Before
25 | fun setup() {
26 | mockWebServer.start(BuildConfig.PORT)
27 | }
28 |
29 | @After
30 | fun teardown() {
31 | mockWebServer.shutdown()
32 | }
33 |
34 | @Test
35 | fun openRandomJokeDialog() {
36 | mockWebServer.dispatcher = SuccessDispatcher()
37 |
38 | dashboardFragmentRobot {
39 | assertButtonRandomJokeDisplayed()
40 | clickButtonRandomJoke()
41 | assertDialogViewRandomJokeDisplayed()
42 | }
43 | }
44 |
45 | @Test
46 | fun dismissesRandomJokeDialogAfterOk() {
47 | mockWebServer.dispatcher = SuccessDispatcher()
48 |
49 | dashboardFragmentRobot {
50 | assertButtonRandomJokeDisplayed()
51 | clickButtonRandomJoke()
52 | assertDialogViewRandomJokeDisplayed()
53 | clickDismissButtonDialog()
54 | }
55 | }
56 |
57 | @Test
58 | fun openCustomJokeScreen() {
59 | dashboardFragmentRobot {
60 | assertButtonCustomJokeDisplayed()
61 | clickButtonCustomJoke()
62 | }
63 |
64 | customFragmentRobot {
65 | assertFirstNameEditTextViewDisplayed()
66 | }
67 | }
68 |
69 | @Test
70 | fun openInfiniteJokesScreen() {
71 | mockWebServer.dispatcher = SuccessDispatcher()
72 |
73 | dashboardFragmentRobot {
74 | assertButtonInfiniteJokesDisplayed()
75 | clickButtonInfiniteJokes()
76 | }
77 |
78 | infiniteFragmentRobot {
79 | assertRecyclerViewDisplayed()
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/detail/DetailFragmentRobot.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.detail
2 |
3 | import androidx.test.espresso.Espresso
4 | import androidx.test.espresso.assertion.ViewAssertions
5 | import androidx.test.espresso.matcher.ViewMatchers
6 | import androidx.test.espresso.matcher.ViewMatchers.withId
7 | import prieto.fernando.jokesapp.R
8 |
9 | fun detailFragmentRobot(func: DetailFragmentRobot.() -> Unit) =
10 | DetailFragmentRobot().apply { func() }
11 |
12 | class DetailFragmentRobot {
13 | fun assertJokeTextViewDisplayed() = apply {
14 | Espresso.onView(DetailFragmentRobot.selectedJokeTextViewMatcher)
15 | .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
16 | }
17 |
18 | fun assertJokeTex(joke: String) = apply {
19 | Espresso.onView(selectedJokeTextViewMatcher)
20 | .check(ViewAssertions.matches(ViewMatchers.withText(joke)))
21 | }
22 |
23 | companion object {
24 | private val selectedJokeTextViewMatcher = withId(R.id.selected_joke)
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/detail/DetailFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.detail
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import okhttp3.mockwebserver.MockWebServer
5 | import org.junit.After
6 | import org.junit.Before
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import prieto.fernando.jokesapp.BuildConfig
11 | import prieto.fernando.jokesapp.dashboard.dashboardFragmentRobot
12 | import prieto.fernando.jokesapp.infinite.infiniteFragmentRobot
13 | import prieto.fernando.jokesapp.utils.TestConfigurationRule
14 | import prieto.fernando.jokesapp.webmock.SuccessDispatcher
15 |
16 | @RunWith(AndroidJUnit4::class)
17 | class DetailFragmentTest {
18 |
19 | @get:Rule
20 | val espressoRule = TestConfigurationRule()
21 |
22 | private val mockWebServer = MockWebServer()
23 |
24 | @Before
25 | fun setup() {
26 | mockWebServer.start(BuildConfig.PORT)
27 | }
28 |
29 | @After
30 | fun teardown() {
31 | mockWebServer.shutdown()
32 | }
33 |
34 | @Test
35 | fun textFieldShown() {
36 | mockWebServer.dispatcher = SuccessDispatcher()
37 |
38 | dashboardFragmentRobot {
39 | assertButtonInfiniteJokesDisplayed()
40 | clickButtonInfiniteJokes()
41 | }
42 |
43 | infiniteFragmentRobot {
44 | assertRecyclerViewDisplayed()
45 | clickItem(1)
46 | }
47 |
48 |
49 | detailFragmentRobot {
50 | assertJokeTextViewDisplayed()
51 | assertJokeTex("Divide Chuck Norris by zero and you will in fact get one........one bad-ass that is.")
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/infinite/InfiniteFragmentRobot.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.infinite
2 |
3 | import androidx.test.espresso.Espresso.onView
4 | import androidx.test.espresso.action.ViewActions
5 | import androidx.test.espresso.assertion.ViewAssertions.matches
6 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
7 | import androidx.test.espresso.matcher.ViewMatchers.withId
8 | import prieto.fernando.jokesapp.R
9 | import prieto.fernando.jokesapp.utils.RecyclerViewItemCountAssertion
10 | import prieto.fernando.jokesapp.utils.RecyclerViewMatcher
11 |
12 | fun infiniteFragmentRobot(func: InfiniteFragmentRobot.() -> Unit) =
13 | InfiniteFragmentRobot().apply { func() }
14 |
15 | class InfiniteFragmentRobot {
16 |
17 | fun assertRecyclerViewDisplayed() = apply {
18 | onView(recyclerViewMatcher).check(matches(isDisplayed()))
19 | }
20 |
21 | fun assertFirstItemsGroup() = apply {
22 | onView(recyclerViewMatcher).check(
23 | RecyclerViewItemCountAssertion(12)
24 | )
25 | }
26 |
27 | fun clickItem(position: Int) = apply {
28 | val itemMatcher = RecyclerViewMatcher(recyclerViewId).atPosition(position)
29 | onView(itemMatcher).perform(ViewActions.click())
30 | }
31 |
32 | companion object {
33 | private const val recyclerViewId = R.id.infinite_jokes_recycler
34 |
35 | private val recyclerViewMatcher = withId(R.id.infinite_jokes_recycler)
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/infinite/InfiniteFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.infinite
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import okhttp3.mockwebserver.MockWebServer
5 | import org.junit.After
6 | import org.junit.Before
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import prieto.fernando.jokesapp.BuildConfig
11 | import prieto.fernando.jokesapp.dashboard.dashboardFragmentRobot
12 | import prieto.fernando.jokesapp.utils.TestConfigurationRule
13 | import prieto.fernando.jokesapp.webmock.SuccessDispatcher
14 |
15 | @RunWith(AndroidJUnit4::class)
16 | class InfiniteFragmentTest {
17 |
18 | @get:Rule
19 | val espressoRule = TestConfigurationRule()
20 |
21 | private val mockWebServer = MockWebServer()
22 |
23 | @Before
24 | fun setup() {
25 | mockWebServer.start(BuildConfig.PORT)
26 | }
27 |
28 | @After
29 | fun teardown() {
30 | mockWebServer.shutdown()
31 | }
32 |
33 | @Test
34 | fun justTwelveItemsListed() {
35 | mockWebServer.dispatcher = SuccessDispatcher()
36 |
37 | dashboardFragmentRobot {
38 | assertButtonInfiniteJokesDisplayed()
39 | clickButtonInfiniteJokes()
40 | }
41 |
42 | infiniteFragmentRobot {
43 | assertRecyclerViewDisplayed()
44 | assertFirstItemsGroup()
45 | }
46 | }
47 |
48 | @Test
49 | fun clickItem() {
50 | mockWebServer.dispatcher = SuccessDispatcher()
51 |
52 | dashboardFragmentRobot {
53 | assertButtonInfiniteJokesDisplayed()
54 | clickButtonInfiniteJokes()
55 | }
56 |
57 | infiniteFragmentRobot {
58 | assertRecyclerViewDisplayed()
59 | clickItem(1)
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/utils/RecyclerViewItemCountAssertion.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.utils
2 |
3 | import android.view.View
4 | import androidx.recyclerview.widget.RecyclerView
5 | import androidx.test.espresso.NoMatchingViewException
6 | import androidx.test.espresso.ViewAssertion
7 | import androidx.test.espresso.matcher.ViewMatchers.assertThat
8 | import org.hamcrest.Matchers
9 |
10 | class RecyclerViewItemCountAssertion(private val expectedCount: Int) : ViewAssertion {
11 |
12 | override fun check(view: View?, noViewFoundException: NoMatchingViewException?) {
13 | noViewFoundException?.let {
14 | throw noViewFoundException
15 | }
16 |
17 | val recyclerView = view as RecyclerView
18 | val adapter = recyclerView.adapter
19 | assertThat(adapter?.itemCount, Matchers.`is`(expectedCount))
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/utils/RecyclerViewMatcher.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.utils
2 |
3 | import android.content.res.Resources
4 | import android.view.View
5 | import androidx.recyclerview.widget.RecyclerView
6 | import org.hamcrest.Description
7 | import org.hamcrest.Matcher
8 | import org.hamcrest.TypeSafeMatcher
9 |
10 | class RecyclerViewMatcher (private val recyclerViewId: Int) {
11 |
12 | fun atPosition(position: Int): Matcher {
13 | return atPositionOnView(position, -1)
14 | }
15 |
16 | fun atPositionOnView(position: Int, targetViewId: Int): Matcher {
17 | return object : TypeSafeMatcher() {
18 | var resources: Resources? = null
19 | var childView: View? = null
20 |
21 | override fun describeTo(description: Description) {
22 | var idDescription = Integer.toString(recyclerViewId)
23 | if (this.resources != null) {
24 | idDescription = try {
25 | this.resources?.getResourceName(recyclerViewId).orEmpty()
26 | } catch (var4: Resources.NotFoundException) {
27 | String.format(
28 | "%s (resource name not found)",
29 | Integer.valueOf(recyclerViewId)
30 | )
31 | }
32 | }
33 |
34 | description.appendText("with id: $idDescription")
35 | }
36 |
37 | public override fun matchesSafely(view: View): Boolean {
38 | this.resources = view.resources
39 |
40 | if (childView == null) {
41 | val recyclerView =
42 | view.rootView.findViewById(recyclerViewId) as RecyclerView
43 | if (recyclerView.id == recyclerViewId) {
44 | childView =
45 | recyclerView.findViewHolderForAdapterPosition(position)?.itemView
46 | } else {
47 | return false
48 | }
49 | }
50 |
51 | return if (targetViewId == -1) {
52 | view === childView
53 | } else {
54 | val targetView = childView?.findViewById(targetViewId)
55 | view === targetView
56 | }
57 | }
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/utils/TestConfigurationRule.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.utils
2 |
3 | import androidx.test.core.app.launchActivity
4 | import org.junit.rules.TestWatcher
5 | import org.junit.runner.Description
6 | import prieto.fernando.jokesapp.view.MainActivity
7 | import prieto.fernando.jokesapp.webmock.injectTestConfiguration
8 |
9 | class TestConfigurationRule : TestWatcher() {
10 | override fun starting(description: Description?) {
11 | super.starting(description)
12 |
13 | injectTestConfiguration {}
14 | launchActivity()
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/webmock/AssetReaderUtil.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.webmock
2 |
3 | import android.content.Context
4 | import java.io.IOException
5 | import java.io.InputStream
6 | import java.io.InputStreamReader
7 |
8 | object AssetReaderUtil {
9 | fun asset(context: Context, assetPath: String): String {
10 | try {
11 | val inputStream = context.assets.open("network_files/$assetPath")
12 | return inputStreamToString(inputStream, "UTF-8")
13 | } catch (e: IOException) {
14 | throw RuntimeException(e)
15 | }
16 | }
17 |
18 | private fun inputStreamToString(inputStream: InputStream, charsetName: String): String {
19 | val builder = StringBuilder()
20 | val reader = InputStreamReader(inputStream, charsetName)
21 | reader.readLines().forEach {
22 | builder.append(it)
23 | }
24 | return builder.toString()
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/webmock/ErrorDispatcher.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.webmock
2 |
3 | import okhttp3.mockwebserver.Dispatcher
4 | import okhttp3.mockwebserver.MockResponse
5 | import okhttp3.mockwebserver.RecordedRequest
6 |
7 | class ErrorDispatcher : Dispatcher() {
8 | override fun dispatch(request: RecordedRequest): MockResponse {
9 | return MockResponse().setResponseCode(404)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/webmock/MockTestRunner.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.webmock
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.os.Bundle
6 | import android.os.StrictMode
7 | import androidx.test.runner.AndroidJUnitRunner
8 | import prieto.fernando.jokesapp.JokesApp
9 |
10 | class MockTestRunner : AndroidJUnitRunner() {
11 | override fun onCreate(arguments: Bundle?) {
12 | StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().permitAll().build())
13 | super.onCreate(arguments)
14 | }
15 |
16 | override fun newApplication(
17 | cl: ClassLoader?,
18 | className: String?,
19 | context: Context?
20 | ): Application {
21 | return super.newApplication(cl, JokesApp::class.java.name, context)
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/webmock/SuccessDispatcher.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.webmock
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import androidx.test.platform.app.InstrumentationRegistry
6 | import okhttp3.mockwebserver.Dispatcher
7 | import okhttp3.mockwebserver.MockResponse
8 | import okhttp3.mockwebserver.RecordedRequest
9 | import prieto.fernando.jokesapp.webmock.AssetReaderUtil.asset
10 |
11 | const val RANDOM_JOKE = "/jokes/random"
12 | const val CUSTOM_JOKE = "/jokes/random?firstName=Fernando&lastName=Prieto"
13 | const val INFINITE_JOKES = "/jokes/random/12"
14 | const val RANDOM_JOKE_SUCCESS = "random_joke_success.json"
15 | const val CUSTOM_JOKE_SUCCESS = "custom_joke_success.json"
16 | const val INFINITE_JOKES_SUCCESS = "joke_list_success.json"
17 |
18 | class SuccessDispatcher(
19 | private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
20 | ) : Dispatcher() {
21 | private val responseFilesByPath: Map = mapOf(
22 | RANDOM_JOKE to RANDOM_JOKE_SUCCESS,
23 | CUSTOM_JOKE to CUSTOM_JOKE_SUCCESS,
24 | INFINITE_JOKES to INFINITE_JOKES_SUCCESS
25 | )
26 |
27 | override fun dispatch(request: RecordedRequest): MockResponse {
28 | val errorResponse = MockResponse().setResponseCode(404)
29 |
30 | val pathWithoutQueryParams = Uri.parse(request.path).path ?: return errorResponse
31 | val responseFile = responseFilesByPath[pathWithoutQueryParams]
32 |
33 | return if (responseFile != null) {
34 | val responseBody = asset(context, responseFile)
35 | MockResponse().setResponseCode(200).setBody(responseBody)
36 | } else {
37 | errorResponse
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/prieto/fernando/jokesapp/webmock/TestConfigurationBuilder.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.webmock
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import prieto.fernando.di.NetworkModule
5 | import prieto.fernando.jokesapp.BuildConfig
6 | import prieto.fernando.jokesapp.JokesApp
7 | import prieto.fernando.jokesapp.di.AppComponent
8 | import prieto.fernando.jokesapp.di.DaggerAppComponent
9 |
10 |
11 | class TestConfigurationBuilder {
12 | private val baseUrl: String ="http://127.0.0.1:${BuildConfig.PORT}"
13 |
14 | fun inject() {
15 | appComponent {
16 | networkModule(NetworkModule(baseUrl))
17 | }.inject(requireTestedApplication())
18 | }
19 | }
20 |
21 | fun injectTestConfiguration(block: TestConfigurationBuilder.() -> Unit) {
22 | TestConfigurationBuilder().apply(block).inject()
23 | }
24 |
25 | private fun appComponent(block: DaggerAppComponent.Builder.() -> Unit = {}): AppComponent =
26 | DaggerAppComponent.builder().apply(block).build()
27 |
28 | private fun requireTestedApplication() =
29 | (InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as JokesApp)
--------------------------------------------------------------------------------
/app/src/debug/assets/network_files/custom_joke_success.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "success",
3 | "value": {
4 | "id": 5,
5 | "joke": "Chuck Norris lost his virginity before his dad did.",
6 | "categories": [
7 | "explicit"
8 | ]
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/debug/assets/network_files/joke_list_success.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "success",
3 | "value": [
4 | {
5 | "id": 44,
6 | "joke": "Chuck Norris is the only man to ever defeat a brick wall in a game of tennis.",
7 | "categories": []
8 | },
9 | {
10 | "id": 390,
11 | "joke": "Divide Chuck Norris by zero and you will in fact get one........one bad-ass that is.",
12 | "categories": []
13 | },
14 | {
15 | "id": 290,
16 | "joke": "There are two types of people in the world... people that suck, and Chuck Norris.",
17 | "categories": []
18 | },
19 | {
20 | "id": 450,
21 | "joke": "Chuck Norris doesn't have disk latency because the hard drive knows to hurry the hell up.",
22 | "categories": [
23 | "nerdy"
24 | ]
25 | },
26 | {
27 | "id": 306,
28 | "joke": "Scientifically speaking, it is impossible to charge Chuck Norris with "obstruction of justice." This is because even Chuck Norris cannot be in two places at the same time.",
29 | "categories": []
30 | },
31 | {
32 | "id": 95,
33 | "joke": "On his birthday, Chuck Norris randomly selects one lucky child to be thrown into the sun.",
34 | "categories": []
35 | },
36 | {
37 | "id": 98,
38 | "joke": "In the beginning there was nothing...then Chuck Norris Roundhouse kicked that nothing in the face and said "Get a job". That is the story of the universe.",
39 | "categories": []
40 | },
41 | {
42 | "id": 45,
43 | "joke": "What was going through the minds of all of Chuck Norris' victims before they died? His shoe.",
44 | "categories": []
45 | },
46 | {
47 | "id": 13,
48 | "joke": "Chuck Norris once challenged Lance Armstrong in a "Who has more testicles?" contest. Chuck Norris won by 5.",
49 | "categories": [
50 | "explicit"
51 | ]
52 | },
53 | {
54 | "id": 135,
55 | "joke": "Chuck Norris roundhouse kicks don't really kill people. They wipe out their entire existence from the space-time continuum.",
56 | "categories": []
57 | },
58 | {
59 | "id": 313,
60 | "joke": "All roads lead to Chuck Norris. And by the transitive property, a roundhouse kick to the face.",
61 | "categories": []
62 | },
63 | {
64 | "id": 465,
65 | "joke": "Whiteboards are white because Chuck Norris scared them that way.",
66 | "categories": []
67 | }
68 | ]
69 | }
--------------------------------------------------------------------------------
/app/src/debug/assets/network_files/random_joke_success.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "success",
3 | "value": {
4 | "id": 5,
5 | "joke": "Chuck Norris lost his virginity before his dad did.",
6 | "categories": [
7 | "explicit"
8 | ]
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/JokesApp.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp
2 |
3 | import dagger.android.AndroidInjector
4 | import dagger.android.DaggerApplication
5 | import prieto.fernando.jokesapp.di.DaggerAppComponent
6 |
7 | open class JokesApp : DaggerApplication() {
8 | override fun applicationInjector(): AndroidInjector =
9 | DaggerAppComponent.builder()
10 | .build()
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/di/ActivityScope.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.di
2 |
3 | import javax.inject.Scope
4 | import kotlin.annotation.Retention
5 |
6 | @Scope
7 | @Retention(AnnotationRetention.RUNTIME)
8 | internal annotation class ActivityScope
9 |
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/di/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.di
2 |
3 | import dagger.Component
4 | import dagger.android.AndroidInjector
5 | import dagger.android.support.AndroidSupportInjectionModule
6 | import di.RepositoryModule
7 | import prieto.fernando.di.ChuckNorrisApiModule
8 | import prieto.fernando.di.NetworkModule
9 | import prieto.fernando.jokesapp.JokesApp
10 | import javax.inject.Singleton
11 |
12 | @Component(
13 | modules = [
14 | AndroidSupportInjectionModule::class,
15 | AppModule::class,
16 | ChuckNorrisApiModule::class,
17 | NetworkModule::class,
18 | RepositoryModule::class,
19 | MainActivityModule::class]
20 | )
21 | @Singleton
22 | interface AppComponent : AndroidInjector
23 |
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.di
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.res.Resources
6 | import dagger.Module
7 | import dagger.Provides
8 | import prieto.fernando.jokesapp.JokesApp
9 | import javax.inject.Singleton
10 | import prieto.fernando.presentation.AppSchedulerProvider
11 | import prieto.fernando.presentation.SchedulerProvider
12 | import javax.inject.Named
13 |
14 | @Module
15 | open class AppModule {
16 |
17 | @Provides
18 | fun provideContext(app: Application): Context = app.applicationContext
19 |
20 | @Provides
21 | fun provideResources(app: Application): Resources = app.resources
22 |
23 | @Provides
24 | @Singleton
25 | fun provideSchedulerProvider(): SchedulerProvider = AppSchedulerProvider()
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/di/MainActivityModule.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.android.ContributesAndroidInjector
6 | import prieto.fernando.jokesapp.view.MainActivity
7 | import prieto.fernando.jokesapp.view.custom.CustomJokeFragment
8 | import prieto.fernando.jokesapp.view.dashboard.DashboardFragment
9 | import prieto.fernando.jokesapp.view.detail.DetailFragment
10 | import prieto.fernando.jokesapp.view.detail.DetailViewModel
11 | import prieto.fernando.jokesapp.view.infinite.InfiniteJokesFragment
12 | import prieto.fernando.presentation.ViewModelProviderFactory
13 | import prieto.fernando.presentation.custom.CustomJokeViewModel
14 | import prieto.fernando.presentation.dashboard.DashboardViewModel
15 | import prieto.fernando.presentation.infinite.InfiniteJokesViewModel
16 | import prieto.fernando.presentation.main.MainViewModel
17 |
18 | @Module
19 | internal abstract class MainActivityModule {
20 | @ActivityScope
21 | @ContributesAndroidInjector
22 | internal abstract fun bindMainActivity(): MainActivity
23 |
24 | @ContributesAndroidInjector
25 | internal abstract fun bindDashboardFragment(): DashboardFragment
26 |
27 | @ContributesAndroidInjector
28 | internal abstract fun bindCustomJokeFragment(): CustomJokeFragment
29 |
30 | @ContributesAndroidInjector
31 | internal abstract fun bindInfiniteJokesFragment(): InfiniteJokesFragment
32 |
33 | @ContributesAndroidInjector
34 | internal abstract fun bindDetailFragment(): DetailFragment
35 |
36 | @Module
37 | companion object {
38 | @Provides
39 | @JvmStatic
40 | internal fun provideMainViewModelFactory(viewModel: MainViewModel): ViewModelProviderFactory {
41 | return ViewModelProviderFactory(viewModel)
42 | }
43 |
44 | @Provides
45 | @JvmStatic
46 | internal fun provideDashboardViewModelFactory(dashboardViewModel: DashboardViewModel): ViewModelProviderFactory {
47 | return ViewModelProviderFactory(dashboardViewModel)
48 | }
49 |
50 | @Provides
51 | @JvmStatic
52 | internal fun provideCustomJokeViewModelFactory(customJokeViewModel: CustomJokeViewModel): ViewModelProviderFactory {
53 | return ViewModelProviderFactory(customJokeViewModel)
54 | }
55 |
56 | @Provides
57 | @JvmStatic
58 | internal fun provideInfiniteJokesViewModelFactory(infiniteJokesViewModel: InfiniteJokesViewModel): ViewModelProviderFactory {
59 | return ViewModelProviderFactory(infiniteJokesViewModel)
60 | }
61 |
62 | @Provides
63 | @JvmStatic
64 | internal fun provideDetailViewModelFactory(detailViewModel: DetailViewModel): ViewModelProviderFactory {
65 | return ViewModelProviderFactory(detailViewModel)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/view/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.view
2 |
3 | import android.os.Bundle
4 | import androidx.lifecycle.ViewModelProviders
5 | import androidx.navigation.Navigation
6 | import androidx.navigation.ui.NavigationUI
7 | import prieto.fernando.jokesapp.R
8 | import prieto.fernando.presentation.main.MainViewModel
9 |
10 | class MainActivity : prieto.fernando.ui.BaseActivity() {
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | setContentView(R.layout.activity_main)
14 | setupNavigation()
15 | }
16 |
17 | private fun setupNavigation() {
18 | val navController = Navigation.findNavController(this, R.id.mainNavigationFragment)
19 | NavigationUI.setupActionBarWithNavController(this, navController)
20 | }
21 |
22 | override fun onSupportNavigateUp() =
23 | Navigation.findNavController(this, R.id.mainNavigationFragment).navigateUp()
24 |
25 | override val viewModel: MainViewModel by lazy {
26 | ViewModelProviders.of(this, vmFactory).get(MainViewModel::class.java)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/view/custom/CustomJokeFragment.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.view.custom
2 |
3 | import android.os.Bundle
4 | import android.text.Editable
5 | import android.text.TextWatcher
6 | import android.view.LayoutInflater
7 | import android.view.ViewGroup
8 | import androidx.lifecycle.ViewModelProviders
9 | import androidx.navigation.fragment.findNavController
10 | import prieto.fernando.jokesapp.R
11 | import prieto.fernando.presentation.custom.CustomJokeViewModel
12 | import prieto.fernando.presentation.custom.model.NamesData
13 | import prieto.fernando.jokesapp.view.extension.observe
14 | import prieto.fernando.ui.BaseFragment
15 | import kotlinx.android.synthetic.main.fragment_custom_joke.button_done as doneButton
16 | import kotlinx.android.synthetic.main.fragment_custom_joke.first_name_text as firstName
17 | import kotlinx.android.synthetic.main.fragment_custom_joke.last_name_text as lastName
18 |
19 | class CustomJokeFragment : BaseFragment() {
20 |
21 | override fun onCreateView(
22 | inflater: LayoutInflater,
23 | container: ViewGroup?,
24 | savedInstanceState: Bundle?
25 | ) = inflater.inflate(R.layout.fragment_custom_joke, container, false)!!
26 |
27 | override fun onResume() {
28 | super.onResume()
29 | setupInputListeners()
30 | }
31 |
32 | private val namesData: NamesData
33 | get() = NamesData(
34 | firstName.text.toString(),
35 | lastName.text.toString()
36 | )
37 |
38 | private val formTextWatcher = object : TextWatcher {
39 | override fun afterTextChanged(s: Editable?) {
40 | }
41 |
42 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
43 | }
44 |
45 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
46 | viewModel.inputs.onNamesChanged(namesData)
47 | }
48 | }
49 |
50 | private fun setupInputListeners() {
51 | bindClickAction(doneButton) {
52 | viewModel.inputs.customRandomJoke(firstName.text.toString(), lastName.text.toString())
53 | }
54 | firstName.addTextChangedListener(formTextWatcher)
55 | lastName.addTextChangedListener(formTextWatcher)
56 | }
57 |
58 | override val viewModel: CustomJokeViewModel by lazy {
59 | ViewModelProviders.of(this, vmFactory).get(CustomJokeViewModel::class.java).apply {
60 | observe(doneButtonEnabled(), ::changeDoneButtonState)
61 | observe(customRandomJokeRetrieved(), ::goBackToDashboard)
62 | observe(errorResource(), ::showErrorToast)
63 | }
64 | }
65 |
66 | private fun goBackToDashboard(unit: Unit?) {
67 | findNavController().popBackStack(R.id.dashboardFragment, false)
68 | }
69 |
70 | private fun changeDoneButtonState(enabled: Boolean?) {
71 | doneButton.isEnabled = enabled ?: false
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/view/dashboard/DashboardFragment.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.view.dashboard
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.lifecycle.ViewModelProviders
7 | import androidx.navigation.fragment.findNavController
8 | import prieto.fernando.jokesapp.R
9 | import prieto.fernando.presentation.dashboard.DashboardViewModel
10 | import prieto.fernando.presentation.data.RandomJokeAndTitleResource
11 | import prieto.fernando.jokesapp.view.extension.observe
12 | import prieto.fernando.ui.BaseFragment
13 | import kotlinx.android.synthetic.main.fragment_dashboard.button_custom_joke as buttonCustomJoke
14 | import kotlinx.android.synthetic.main.fragment_dashboard.button_multiple_jokes as buttonMultipleJoke
15 | import kotlinx.android.synthetic.main.fragment_dashboard.button_random_joke as buttonRandomJoke
16 |
17 | class DashboardFragment : BaseFragment() {
18 |
19 | override fun onCreateView(
20 | inflater: LayoutInflater,
21 | container: ViewGroup?,
22 | savedInstanceState: Bundle?
23 | ) = inflater.inflate(R.layout.fragment_dashboard, container, false)!!
24 |
25 | override fun onResume() {
26 | super.onResume()
27 | setupInputListeners()
28 | viewModel.inputs.customRandomJokeForDialog()
29 | }
30 |
31 | private fun setupInputListeners() {
32 | bindClickAction(buttonRandomJoke) {
33 | viewModel.inputs.randomJoke()
34 | }
35 | bindClickAction(buttonCustomJoke) {
36 | viewModel.inputs.onCustomRandomJokeClicked()
37 | }
38 | bindClickAction(buttonMultipleJoke) {
39 | viewModel.inputs.onMultipleJokesClicked()
40 | }
41 | }
42 |
43 | override val viewModel: DashboardViewModel by lazy {
44 | ViewModelProviders.of(this, vmFactory).get(DashboardViewModel::class.java).apply {
45 | observe(navigateToCustomJoke(), ::navigateToCustomJokeFragment)
46 | observe(navigateToInfiniteJokes(), ::navigateToInfiniteJokesFragment)
47 | observe(customRandomJokeRetrieved(), ::resetCacheAndShowDialog)
48 | observe(randomJokeRetrieved(), ::showRandomJokeDialog)
49 | observe(errorResource(), ::showErrorToast)
50 | }
51 | }
52 |
53 | private fun navigateToCustomJokeFragment(unit: Unit?) {
54 | findNavController().navigate(R.id.goToCustomJokeFragment)
55 | }
56 |
57 | private fun navigateToInfiniteJokesFragment(unit: Unit?) {
58 | findNavController().navigate(R.id.goToInfiniteJokesFragment)
59 | }
60 |
61 | private fun resetCacheAndShowDialog(customRandomJoke: RandomJokeAndTitleResource?) {
62 | customRandomJoke?.let {
63 | viewModel.inputs.resetCustomJokeCache()
64 | showDialog(
65 | customRandomJoke.titleResource,
66 | customRandomJoke.randomJokeUiModel.joke
67 | )
68 | }
69 | }
70 |
71 | private fun showRandomJokeDialog(randomJokeAndTitle: RandomJokeAndTitleResource?) {
72 | randomJokeAndTitle?.let {
73 | showDialog(
74 | randomJokeAndTitle.titleResource,
75 | randomJokeAndTitle.randomJokeUiModel.joke
76 | )
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/view/detail/DetailFragment.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.view.detail
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.lifecycle.ViewModelProviders
7 | import kotlinx.android.synthetic.main.fragment_detail.*
8 | import prieto.fernando.jokesapp.R
9 | import prieto.fernando.ui.BaseFragment
10 |
11 | class DetailFragment : BaseFragment() {
12 |
13 | override fun onCreateView(
14 | inflater: LayoutInflater,
15 | container: ViewGroup?,
16 | savedInstanceState: Bundle?
17 | ) = inflater.inflate(R.layout.fragment_detail, container, false)!!
18 |
19 | override fun onResume() {
20 | super.onResume()
21 | getBundledContents()
22 | }
23 |
24 | private fun getBundledContents() {
25 | arguments?.let { bundle ->
26 | val joke = bundle.getString(DETAIL_FRAGMENT_TYPE_ARG)
27 | ?: throw IllegalStateException("Joke should be provided")
28 | selected_joke.text = joke
29 | }
30 | }
31 |
32 | override val viewModel: DetailViewModel by lazy {
33 | ViewModelProviders.of(this, vmFactory).get(DetailViewModel::class.java)
34 | }
35 |
36 | companion object {
37 | const val DETAIL_FRAGMENT_TYPE_ARG = "joke"
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/view/detail/DetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.view.detail
2 |
3 | import prieto.fernando.presentation.BaseViewModel
4 | import javax.inject.Inject
5 |
6 | class DetailViewModel @Inject constructor() : BaseViewModel(){
7 |
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/view/extension/Lifecycle.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.view.extension
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.Observer
6 |
7 | fun > LifecycleOwner.observe(liveData: L, body: (T?) -> Unit) =
8 | liveData.observe(this, Observer(body))
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/view/infinite/InfiniteJokesFragment.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.view.infinite
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.lifecycle.ViewModelProviders
8 | import androidx.navigation.fragment.findNavController
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import kotlinx.android.synthetic.main.fragment_infinite_jokes.*
11 | import prieto.fernando.jokesapp.R
12 | import prieto.fernando.jokesapp.view.detail.DetailFragment
13 | import prieto.fernando.jokesapp.view.extension.observe
14 | import prieto.fernando.jokesapp.view.infinite.adapter.ClickListener
15 | import prieto.fernando.jokesapp.view.infinite.adapter.JokesAdapter
16 | import prieto.fernando.jokesapp.view.infinite.widget.InfiniteScrollListener
17 | import prieto.fernando.presentation.RandomJokeUiModel
18 | import prieto.fernando.presentation.infinite.InfiniteJokesViewModel
19 | import prieto.fernando.ui.BaseFragment
20 | import kotlinx.android.synthetic.main.fragment_infinite_jokes.infinite_jokes_recycler as infiniteRecyclerView
21 |
22 | class InfiniteJokesFragment : BaseFragment(), ClickListener {
23 |
24 | private var jokesAdapter: JokesAdapter? = null
25 |
26 | override fun onItemClicked(joke: String) {
27 | viewModel.onJokeSelected(joke)
28 | }
29 |
30 | override fun onCreateView(
31 | inflater: LayoutInflater,
32 | container: ViewGroup?,
33 | savedInstanceState: Bundle?
34 | ) = inflater.inflate(R.layout.fragment_infinite_jokes, container, false)!!
35 |
36 | private var isLoading = false
37 |
38 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
39 | super.onViewCreated(view, savedInstanceState)
40 | setupRecyclerView()
41 | }
42 |
43 | private fun setupRecyclerView() {
44 | jokesAdapter = JokesAdapter(this)
45 | infiniteRecyclerView.adapter = jokesAdapter
46 | val linearLayoutManager = LinearLayoutManager(context)
47 | infiniteRecyclerView.layoutManager = linearLayoutManager
48 | val endlessScrollListener = object : InfiniteScrollListener(linearLayoutManager) {
49 | override fun onLoadMore() {
50 | isLoading = true
51 | viewModel.multipleRandomJokes()
52 | }
53 |
54 | override fun isLoading(): Boolean {
55 | return isLoading
56 | }
57 | }
58 | infiniteRecyclerView.addOnScrollListener(endlessScrollListener)
59 |
60 | }
61 |
62 | override fun onResume() {
63 | super.onResume()
64 | viewModel.inputs.multipleRandomJokes()
65 | }
66 |
67 | override val viewModel: InfiniteJokesViewModel by lazy {
68 | ViewModelProviders.of(this, vmFactory).get(InfiniteJokesViewModel::class.java).apply {
69 | observe(multipleRandomJokesRetrieved(), ::addJokesToAdapter)
70 | observe(errorResource(), ::showErrorToast)
71 | observe(loading(), ::showLoading)
72 | observe(jokeSelected(), ::navigateToDetailFragment)
73 | }
74 | }
75 |
76 | private fun addJokesToAdapter(randomJokes: List?) {
77 | randomJokes?.let {
78 | isLoading = false
79 | jokesAdapter?.let { adapter ->
80 | adapter.setData(randomJokes)
81 | infiniteRecyclerView.adapter = adapter
82 | }
83 | }
84 | }
85 |
86 | private fun showLoading(isLoading: Boolean?) {
87 | isLoading?.let {
88 | progressBar.visibility = if (isLoading) {
89 | View.VISIBLE
90 | } else {
91 | View.GONE
92 | }
93 | }
94 | }
95 |
96 | private fun navigateToDetailFragment(joke: String?) {
97 | val args = Bundle().apply {
98 | putString(DetailFragment.DETAIL_FRAGMENT_TYPE_ARG, joke)
99 | }
100 | findNavController().navigate(R.id.goToDetailFragment, args)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/view/infinite/adapter/JokesAdapter.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.view.infinite.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.RecyclerView
7 | import kotlinx.android.synthetic.main.item_joke.view.*
8 | import prieto.fernando.jokesapp.R
9 | import prieto.fernando.presentation.RandomJokeUiModel
10 |
11 | interface BindableAdapter {
12 | fun setData(data: T)
13 | }
14 |
15 | interface ClickListener {
16 | fun onItemClicked(joke: String)
17 | }
18 |
19 | class JokesAdapter (private val clickListener: ClickListener): RecyclerView.Adapter(),
20 | BindableAdapter> {
21 | private val jokes = mutableListOf()
22 |
23 | override fun setData(data: List) {
24 | jokes.addAll(data)
25 | notifyDataSetChanged()
26 | }
27 |
28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): JokeHolder {
29 | val inflater = LayoutInflater.from(parent.context)
30 | return JokeHolder(inflater.inflate(R.layout.item_joke, parent, false))
31 | }
32 |
33 | override fun getItemCount() = jokes.size
34 |
35 | override fun onBindViewHolder(holder: JokeHolder, position: Int) {
36 | holder.bind(jokes[position], clickListener)
37 | }
38 |
39 | class JokeHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
40 | fun bind(randomJokeUiModel: RandomJokeUiModel, clickListener: ClickListener) {
41 | itemView.item_joke.text = randomJokeUiModel.joke
42 | itemView.setOnClickListener {
43 | clickListener.onItemClicked(randomJokeUiModel.joke)
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/prieto/fernando/jokesapp/view/infinite/widget/InfiniteScrollListener.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.jokesapp.view.infinite.widget
2 |
3 | import androidx.recyclerview.widget.LinearLayoutManager
4 | import androidx.recyclerview.widget.RecyclerView
5 |
6 | abstract class InfiniteScrollListener(
7 | private val linearLayoutManager: LinearLayoutManager
8 | ) : RecyclerView.OnScrollListener() {
9 |
10 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
11 | if (dy > 0) {
12 | val visibleItemCount = linearLayoutManager.childCount
13 | val totalItemCount = linearLayoutManager.itemCount
14 | val firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition()
15 | if (!isLoading()) {
16 | if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
17 | && firstVisibleItemPosition >= 0
18 | ) {
19 | onLoadMore()
20 | }
21 | }
22 | }
23 | }
24 |
25 | abstract fun onLoadMore()
26 | abstract fun isLoading(): Boolean
27 |
28 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_custom_joke.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
26 |
27 |
36 |
37 |
38 |
39 |
47 |
48 |
56 |
57 |
66 |
67 |
68 |
69 |
70 |
83 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_dashboard.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
22 |
23 |
35 |
36 |
48 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
19 |
20 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_infinite_jokes.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
19 |
20 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_joke.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
19 |
20 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FB8C00
4 | #C25E00
5 | #FFBD45
6 |
7 | #E32747
8 | #2D2D2D
9 | #F1F1F1
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0dp
4 | 1dp
5 | 4dp
6 | 8dp
7 | 10dp
8 | 16dp
9 | 20dp
10 | 30dp
11 | 40dp
12 | 64dp
13 |
14 |
15 | 44sp
16 | 36sp
17 | 30sp
18 | 24sp
19 | 18sp
20 | 15sp
21 | 12sp
22 |
23 |
24 | 2dp
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FB8C00
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | JokesApp
3 |
4 | RANDOM JOKE
5 | CUSTOM JOKE
6 | INFINITE JOKES
7 |
8 |
9 | First Name
10 | Last Name
11 | DONE
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/base-android-library.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | apply plugin: 'kotlin-kapt'
8 |
9 | android {
10 | def config = rootProject.extensions.getByName("ext")
11 |
12 | compileSdkVersion config["compile_sdk"]
13 | buildToolsVersion config["build_version"]
14 |
15 | defaultConfig {
16 | minSdkVersion config["min_sdk"]
17 | targetSdkVersion config["target_sdk"]
18 | versionCode config["version_code"]
19 | versionName config["version_name"]
20 |
21 | testInstrumentationRunner config["test_runner"]
22 | consumerProguardFiles 'consumer-rules.pro'
23 | }
24 |
25 | compileOptions {
26 | sourceCompatibility 1.8
27 | targetCompatibility 1.8
28 | }
29 |
30 | buildTypes {
31 | release {
32 | minifyEnabled false
33 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
34 | }
35 | }
36 | }
37 |
38 | dependencies {
39 | def applicationDependencies = rootProject.ext.mainApplication
40 | def unitTestDependencies = rootProject.ext.unitTesting
41 |
42 | // Application dependencies
43 | implementation applicationDependencies.kotlin
44 | implementation applicationDependencies.appCompat
45 | implementation applicationDependencies.coreKtx
46 |
47 | // Compile time dependencies
48 | kapt applicationDependencies.daggerCompiler
49 | kapt applicationDependencies.daggerAndroidProcessor
50 |
51 | // Application dependencies
52 | implementation applicationDependencies.daggerAndroid
53 | implementation applicationDependencies.daggerAndroidSupport
54 | implementation applicationDependencies.rxJava
55 | implementation applicationDependencies.rxKotlin
56 | implementation applicationDependencies.rxAndroid
57 | implementation applicationDependencies.rxBinding
58 |
59 | // Unit/Integration tests dependencies
60 | testImplementation unitTestDependencies.junit
61 | testImplementation unitTestDependencies.mockitoCore
62 | testImplementation unitTestDependencies.mockitoInline
63 | testImplementation unitTestDependencies.mockitoKotlin
64 | testImplementation unitTestDependencies.junitPlatformRunner
65 | testImplementation unitTestDependencies.commonsCodec
66 | testImplementation unitTestDependencies.coreTesting
67 | }
68 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: 'buildsystem/dependencies.gradle'
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.3.50'
5 |
6 | ext.build_version = '29.0.2'
7 | ext.compile_sdk = 29
8 | ext.target_sdk = 29
9 | ext.min_sdk = 16
10 | ext.application_id = 'prieto.fernando.jokesapp'
11 | ext.version_name = '1.0'
12 | ext.version_code = 1
13 | ext.test_runner = 'prieto.fernando.jokesapp.webmock.MockTestRunner'
14 |
15 |
16 | repositories {
17 | google()
18 | jcenter()
19 | maven { url "https://plugins.gradle.org/m2/" }
20 | maven { url "https://maven.fabric.io/public" }
21 | maven { url "https://jitpack.io" }
22 | }
23 | dependencies {
24 | classpath 'com.android.tools.build:gradle:3.5.2'
25 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
26 | classpath "org.jmailen.gradle:kotlinter-gradle:2.1.2"
27 | classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0"
28 | }
29 | }
30 |
31 | allprojects {
32 | repositories {
33 | google()
34 | jcenter()
35 | maven { url "https://jitpack.io" }
36 | }
37 | }
38 |
39 | task clean(type: Delete) {
40 | delete rootProject.buildDir
41 | }
42 |
--------------------------------------------------------------------------------
/buildsystem/dependencies.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | jcenter()
5 | }
6 | gradle.projectsEvaluated {
7 | tasks.withType(JavaCompile) {
8 | options.compilerArgs << "-Xlint:deprecation"
9 | }
10 | }
11 | }
12 |
13 | ext {
14 | // Android libraries
15 | appCompat_version = '1.1.0'
16 | constraintLayout_version = '1.1.3'
17 | archComponents_version = '2.1.0'
18 | recyclerview_version = '1.0.0'
19 | cardview_version = '1.0.0'
20 |
21 | // Navigation
22 | navigation_ktx_version = '1.0.0'
23 |
24 | // Dependency Injection
25 | dagger_version = '2.16'
26 | dagger_compiler_version = '2.16'
27 |
28 | // Retrofit
29 | retrofit_version = '2.5.0'
30 | retrofit_converter_gson_version = '2.4.0'
31 | retrofit_rxjava2_adapter_version = '1.0.0'
32 |
33 | // OkHttp
34 | okhttp_logging_interceptor_version = '4.2.1'
35 |
36 | // Rx
37 | rxjava_version = '2.2.7'
38 | rxkotlin_version = '2.3.0'
39 | rxandroid_version = '2.1.1'
40 | rxbinding_version = '2.2.0'
41 |
42 | // Build Android and Dagger with JDK9+
43 | jdk9_builder_version = '1.0'
44 |
45 | // Loggin
46 | timber_version = '4.7.1'
47 |
48 | //Unit Testing
49 | junit_version = '4.12'
50 | mockito_version = '2.27.0'
51 | mockito_kotlin_version = '1.5.0'
52 | junit_platform_runner_version = '1.0.2'
53 | commons_codec_version = '1.12'
54 | core_testing_version = '1.1.1'
55 |
56 | //Acceptance Testing
57 | androidX_test_version = '1.2.0'
58 | espresso_version = '3.2.0'
59 | test_ext_version = '1.1.1'
60 | fragment_testing_version = '1.1.1'
61 | espresso_contrib_version = '3.0.2'
62 | web_mock_server_version = '4.2.1'
63 |
64 | //Development
65 | leakCanaryVersion = '1.5'
66 |
67 | mainApplication = [
68 | kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version",
69 | appCompat : "androidx.appcompat:appcompat:$appCompat_version",
70 | coreKtx : "androidx.core:core-ktx:$appCompat_version",
71 | constraintLayout : "androidx.constraintlayout:constraintlayout:$constraintLayout_version",
72 | recyclerView : "androidx.recyclerview:recyclerview:$recyclerview_version",
73 | cardView : "androidx.cardview:cardview:$cardview_version",
74 | archViewModel : "androidx.lifecycle:lifecycle-viewmodel:$archComponents_version",
75 | archComponents : "androidx.lifecycle:lifecycle-extensions:$archComponents_version",
76 | lifecycleCompiler : "androidx.lifecycle:lifecycle-compiler:$archComponents_version",
77 | archComponentsCompiler : "android.arch.lifecycle:compiler:$archComponents_version",
78 | archNavigationFragment : "android.arch.navigation:navigation-fragment-ktx:$navigation_ktx_version",
79 | archNavigationUi : "android.arch.navigation:navigation-ui-ktx:$navigation_ktx_version",
80 | daggerAndroid : "com.google.dagger:dagger-android:$dagger_version",
81 | daggerAndroidSupport : "com.google.dagger:dagger-android-support:$dagger_version",
82 | daggerCompiler : "com.google.dagger:dagger-compiler:$dagger_version",
83 | daggerAndroidProcessor : "com.google.dagger:dagger-android-processor:$dagger_version",
84 | retrofit : "com.squareup.retrofit2:retrofit:$retrofit_version",
85 | retrofitConverterGson : "com.squareup.retrofit2:converter-gson:$retrofit_converter_gson_version",
86 | retrofitRxjava2Adapter : "com.jakewharton.retrofit:retrofit2-rxjava2-adapter:$retrofit_rxjava2_adapter_version",
87 | okHttpLoggingInterceptor: "com.squareup.okhttp3:logging-interceptor:$okhttp_logging_interceptor_version",
88 | rxJava : "io.reactivex.rxjava2:rxjava:$rxjava_version",
89 | rxKotlin : "io.reactivex.rxjava2:rxkotlin:$rxkotlin_version",
90 | rxAndroid : "io.reactivex.rxjava2:rxandroid:$rxandroid_version",
91 | rxBinding : "com.jakewharton.rxbinding2:rxbinding-appcompat-v7-kotlin:$rxbinding_version",
92 | timber : "com.jakewharton.timber:timber:$timber_version",
93 | jdk9Builder : "com.github.pengrad:jdk9-deps:$jdk9_builder_version"
94 | ]
95 |
96 | unitTesting = [
97 | junit : "junit:junit:$junit_version",
98 | mockitoCore : "org.mockito:mockito-core:$mockito_version",
99 | mockitoInline : "org.mockito:mockito-inline:$mockito_version",
100 | mockitoKotlin : "com.nhaarman:mockito-kotlin:$mockito_kotlin_version",
101 | junitPlatformRunner: "org.junit.platform:junit-platform-runner:$junit_platform_runner_version",
102 | commonsCodec : "commons-codec:commons-codec:$commons_codec_version",
103 | coreTesting : "android.arch.core:core-testing:$core_testing_version"
104 | ]
105 |
106 | acceptanceTesting = [
107 | testEspressoContrib: "com.android.support.test.espresso:espresso-contrib:$espresso_contrib_version",
108 | testRunner : "androidx.test:runner:$androidX_test_version",
109 | testRules : "androidx.test:rules:$androidX_test_version",
110 | testExt : "androidx.test.ext:junit:$test_ext_version",
111 | fragmentTesting : "androidx.fragment:fragment-testing:$fragment_testing_version",
112 | webMockServer : "com.squareup.okhttp3:mockwebserver:$web_mock_server_version",
113 | testCore : "androidx.test:core:$androidX_test_version",
114 | coreKtx : "androidx.test:core-ktx:$androidX_test_version"
115 | ]
116 | }
117 |
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/core/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: "$rootDir/base-android-library.gradle"
2 |
3 | dependencies {
4 | def applicationDependencies = rootProject.ext.mainApplication
5 |
6 | // Compile time dependencies
7 | kapt applicationDependencies.lifecycleCompiler
8 |
9 | // Application dependencies
10 | implementation applicationDependencies.archViewModel
11 | implementation applicationDependencies.archComponents
12 | implementation applicationDependencies.archNavigationFragment
13 | implementation applicationDependencies.archNavigationUi
14 | }
--------------------------------------------------------------------------------
/core/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/core/consumer-rules.pro
--------------------------------------------------------------------------------
/core/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/core/src/androidTest/java/prieto/fernando/core/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.core
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("prieto.fernando.core.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/core/src/main/java/prieto/fernando/presentation/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.ViewModel
6 | import io.reactivex.Observable
7 | import io.reactivex.disposables.CompositeDisposable
8 | import io.reactivex.subjects.BehaviorSubject
9 | import io.reactivex.subjects.PublishSubject
10 | import io.reactivex.subjects.Subject
11 | import javax.inject.Inject
12 |
13 | interface BaseViewModelInputs
14 |
15 | interface BaseViewModelOutputs {
16 | fun error(): Observable
17 | fun finish(): Observable
18 | fun refreshing(): Observable
19 | }
20 |
21 | open class BaseViewModel() : ViewModel(),
22 | BaseViewModelInputs,
23 | BaseViewModelOutputs {
24 |
25 | @Inject
26 | lateinit var schedulerProvider: SchedulerProvider
27 |
28 | protected val subscriptions = CompositeDisposable()
29 |
30 | override fun onCleared() {
31 | super.onCleared()
32 | subscriptions.clear()
33 | }
34 |
35 | open val inputs: BaseViewModelInputs
36 | get() = this
37 |
38 | open val outputs: BaseViewModelOutputs
39 | get() = this
40 |
41 | protected val finish: Subject = PublishSubject.create()
42 | protected val error: Subject = PublishSubject.create()
43 | protected val refreshing: Subject = BehaviorSubject.createDefault(false)
44 |
45 | override fun error(): Observable {
46 | return error.observeOn(schedulerProvider.ui())
47 | .hide()
48 | }
49 |
50 | override fun finish(): Observable {
51 | return finish.observeOn(schedulerProvider.ui())
52 | .hide()
53 | }
54 |
55 | override fun refreshing(): Observable {
56 | return refreshing.observeOn(schedulerProvider.ui())
57 | .hide()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/core/src/main/java/prieto/fernando/presentation/SchedulerProvider.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation
2 |
3 | import io.reactivex.CompletableTransformer
4 | import io.reactivex.FlowableTransformer
5 | import io.reactivex.MaybeTransformer
6 | import io.reactivex.ObservableTransformer
7 | import io.reactivex.Scheduler
8 | import io.reactivex.SingleTransformer
9 | import io.reactivex.android.schedulers.AndroidSchedulers
10 | import io.reactivex.schedulers.Schedulers
11 |
12 | interface SchedulerProvider {
13 |
14 | fun io(): Scheduler
15 | fun ui(): Scheduler
16 | fun computation(): Scheduler
17 | fun newThread(): Scheduler
18 |
19 | fun doOnIoObserveOnMainObservable(): ObservableTransformer
20 | fun doOnIoObserveOnMainSingle(): SingleTransformer
21 | fun doOnIoObserveOnMainMaybe(): MaybeTransformer
22 | fun doOnIoObservable(): ObservableTransformer
23 | fun doOnIoSingle(): SingleTransformer
24 | fun doOnIoMaybe(): MaybeTransformer
25 | fun doOnIoSingleOnMainObservable(): SingleTransformer
26 | fun doOnComputationObserveOnMainSingle(): SingleTransformer
27 | fun doOnIoObserveOnMainFlowable(): FlowableTransformer
28 | fun doOnIoCompletable(): CompletableTransformer
29 | fun doOnComputationCompletable(): CompletableTransformer
30 | fun doOnIoObserveOnMainCompletable(): CompletableTransformer
31 | }
32 |
33 | abstract class BaseSchedulerProvider :
34 | SchedulerProvider {
35 |
36 | override fun doOnIoObserveOnMainObservable(): ObservableTransformer {
37 | return ObservableTransformer {
38 | it.subscribeOn(io())
39 | .unsubscribeOn(io())
40 | .observeOn(ui())
41 | }
42 | }
43 |
44 | override fun doOnIoObserveOnMainSingle(): SingleTransformer {
45 | return SingleTransformer {
46 | it.subscribeOn(io())
47 | .unsubscribeOn(io())
48 | .observeOn(ui())
49 | }
50 | }
51 |
52 | override fun doOnIoObserveOnMainMaybe(): MaybeTransformer {
53 | return MaybeTransformer {
54 | it.subscribeOn(io())
55 | .unsubscribeOn(io())
56 | .observeOn(ui())
57 | }
58 | }
59 |
60 | override fun doOnIoObservable(): ObservableTransformer {
61 | return ObservableTransformer {
62 | it.subscribeOn(io())
63 | .unsubscribeOn(io())
64 | }
65 | }
66 |
67 | override fun doOnIoSingle(): SingleTransformer {
68 | return SingleTransformer {
69 | it.subscribeOn(io())
70 | .unsubscribeOn(io())
71 | }
72 | }
73 |
74 | override fun doOnIoMaybe(): MaybeTransformer {
75 | return MaybeTransformer {
76 | it.subscribeOn(io())
77 | .unsubscribeOn(io())
78 | }
79 | }
80 |
81 | override fun doOnIoSingleOnMainObservable(): SingleTransformer {
82 | return SingleTransformer {
83 | it.subscribeOn(io())
84 | .unsubscribeOn(io())
85 | .observeOn(ui())
86 | }
87 | }
88 |
89 | override fun doOnIoCompletable(): CompletableTransformer {
90 | return CompletableTransformer {
91 | it.subscribeOn(io())
92 | .unsubscribeOn(io())
93 | }
94 | }
95 |
96 | override fun doOnComputationCompletable(): CompletableTransformer {
97 | return CompletableTransformer {
98 | it.subscribeOn(computation())
99 | .unsubscribeOn(computation())
100 | }
101 | }
102 |
103 | override fun doOnComputationObserveOnMainSingle(): SingleTransformer {
104 | return SingleTransformer {
105 | it.subscribeOn(computation())
106 | .unsubscribeOn(computation())
107 | .observeOn(ui())
108 | }
109 | }
110 |
111 | override fun doOnIoObserveOnMainCompletable(): CompletableTransformer {
112 | return CompletableTransformer {
113 | it.subscribeOn(io())
114 | .unsubscribeOn(io())
115 | .observeOn(ui())
116 | }
117 | }
118 |
119 | override fun doOnIoObserveOnMainFlowable(): FlowableTransformer {
120 | return FlowableTransformer {
121 | it.subscribeOn(io())
122 | .unsubscribeOn(io())
123 | .observeOn(ui())
124 | }
125 | }
126 | }
127 |
128 | class AppSchedulerProvider : BaseSchedulerProvider() {
129 | override fun io(): Scheduler = Schedulers.io()
130 | override fun ui(): Scheduler = AndroidSchedulers.mainThread()
131 | override fun computation() = Schedulers.computation()
132 | override fun newThread() = Schedulers.newThread()
133 | }
134 |
--------------------------------------------------------------------------------
/core/src/main/java/prieto/fernando/presentation/ViewModelProviderFactory.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 |
6 | @Suppress("UNCHECKED_CAST")
7 | class ViewModelProviderFactory(private val mViewModel: VMType) : ViewModelProvider.Factory {
8 |
9 | @SuppressWarnings("UNCHECKED_CAST")
10 | override fun create(modelClass: Class): T {
11 | if (modelClass.isAssignableFrom(mViewModel::class.java)) {
12 | return mViewModel as T
13 | }
14 |
15 | throw IllegalArgumentException("Unknown class name")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/core/src/main/java/prieto/fernando/ui/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.ui
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.android.support.DaggerAppCompatActivity
5 | import io.reactivex.disposables.CompositeDisposable
6 | import javax.inject.Inject
7 | import prieto.fernando.presentation.SchedulerProvider
8 | import prieto.fernando.presentation.ViewModelProviderFactory
9 |
10 | abstract class BaseActivity : DaggerAppCompatActivity(),
11 | BaseView {
12 |
13 | @Inject
14 | protected lateinit var vmFactory: ViewModelProviderFactory
15 |
16 | @Inject
17 | protected lateinit var schedulers: SchedulerProvider
18 |
19 | protected val subscriptions = CompositeDisposable()
20 |
21 | override fun onDestroy() {
22 | subscriptions.clear()
23 | super.onDestroy()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/core/src/main/java/prieto/fernando/ui/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.ui
2 |
3 | import android.app.AlertDialog
4 | import android.view.View
5 | import android.widget.Toast
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.ViewModelProviders
9 | import androidx.navigation.fragment.findNavController
10 | import androidx.navigation.ui.NavigationUI
11 | import com.jakewharton.rxbinding2.view.RxView
12 | import dagger.android.support.DaggerFragment
13 | import io.reactivex.disposables.CompositeDisposable
14 | import prieto.fernando.core.R
15 | import java.util.concurrent.TimeUnit
16 | import javax.inject.Inject
17 | import prieto.fernando.presentation.BaseViewModel
18 | import prieto.fernando.presentation.SchedulerProvider
19 | import prieto.fernando.presentation.ViewModelProviderFactory
20 |
21 | abstract class BaseFragment : DaggerFragment(), BaseView {
22 |
23 | @Inject
24 | protected lateinit var vmFactory: ViewModelProviderFactory
25 |
26 | @Inject
27 | lateinit var schedulers: SchedulerProvider
28 |
29 | protected val subscriptions = CompositeDisposable()
30 |
31 | protected fun showDialog(titleResource: Int, jokeContent: String) {
32 | AlertDialog.Builder(context)
33 | .setTitle(titleResource)
34 | .setMessage(jokeContent)
35 | .setPositiveButton(R.string.dashboard_dialog_dismiss_button) { dialog, _ ->
36 | dialog.dismiss()
37 | }
38 | .setCancelable(true)
39 | .show()
40 | }
41 |
42 | protected fun showErrorToast(errorResource: Int?) {
43 | errorResource?.let {
44 | val errorMessage = resources.getString(errorResource)
45 | Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT)
46 | }
47 | }
48 |
49 | protected fun bindClickAction(view: View, clickAction: () -> Unit) {
50 | RxView.clicks(view)
51 | .throttleFirst(BUTTON_DEBOUNCE_TIMEOUT_MS, TimeUnit.MILLISECONDS)
52 | .subscribe { clickAction() }
53 | .also { subscriptions.add(it) }
54 | }
55 |
56 | override fun onPause() {
57 | subscriptions.clear()
58 | super.onPause()
59 | }
60 |
61 | protected fun setupNavigation() {
62 | activity?.let {
63 | NavigationUI.setupActionBarWithNavController(
64 | this.activity as AppCompatActivity,
65 | findNavController()
66 | )
67 | }
68 | }
69 |
70 | protected fun getViewModel(viewModelClass: Class) =
71 | ViewModelProviders.of(this, vmFactory).get(viewModelClass)
72 |
73 | companion object {
74 | const val BUTTON_DEBOUNCE_TIMEOUT_MS = 500L
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/core/src/main/java/prieto/fernando/ui/BaseView.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.ui
2 |
3 | import androidx.lifecycle.ViewModel
4 |
5 | interface BaseView {
6 | val viewModel: T
7 | }
8 |
--------------------------------------------------------------------------------
/core/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | core
3 |
4 |
5 | DISMISS
6 | Random Joke
7 | Please enter a valid first name
8 | Please enter a valid last name
9 |
10 |
11 | There was an error when a joke was being retrieved
12 |
13 |
14 | There was an error when a custom joke was being retrieved
15 | Custom Joke
16 |
17 |
18 | There was an error when the jokes were being retrieved
19 |
20 |
21 |
--------------------------------------------------------------------------------
/data-cache/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/data-cache/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: "$rootDir/base-android-library.gradle"
2 |
--------------------------------------------------------------------------------
/data-cache/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/data-cache/consumer-rules.pro
--------------------------------------------------------------------------------
/data-cache/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/data-cache/src/androidTest/java/prieto/fernando/data_cache/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.data_cache
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("prieto.fernando.data_cache.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/data-cache/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/data-cache/src/main/java/prieto/fernando/model/RandomJokeLocalModel.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.model
2 |
3 | data class RandomJokeLocalModel(
4 | val id: String,
5 | val joke: String,
6 | val categories: List
7 | )
8 |
9 | enum class CategoryLocalModel {
10 | EXPLICIT,
11 | NERDY,
12 | UNKNOWN
13 | }
14 |
--------------------------------------------------------------------------------
/data-cache/src/main/java/prieto/fernando/source/JokesLocalSource.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.source
2 |
3 | import io.reactivex.Single
4 | import io.reactivex.subjects.BehaviorSubject
5 | import prieto.fernando.model.RandomJokeLocalModel
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class JokesLocalSource @Inject constructor() {
11 | private val customJokeSubject = BehaviorSubject.createDefault(getEmptyRandomJoke())
12 | private var hasCustomJokesValidData = false
13 |
14 | fun setCustomJoke(randomJokeLocalModel: RandomJokeLocalModel) {
15 | hasCustomJokesValidData = true
16 | customJokeSubject.onNext(randomJokeLocalModel)
17 | }
18 |
19 | fun getCustomJoke(): Single =
20 | customJokeSubject.distinctUntilChanged().firstOrError()
21 |
22 | fun hasCustomJokesValidData(): Boolean = hasCustomJokesValidData
23 |
24 | fun resetData(): Single {
25 | hasCustomJokesValidData = false
26 | customJokeSubject.onNext(getEmptyRandomJoke())
27 | return Single.just(Unit)
28 | }
29 |
30 | private fun getEmptyRandomJoke() = RandomJokeLocalModel("", "", emptyList())
31 | }
32 |
--------------------------------------------------------------------------------
/data-cache/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | data-cache
3 |
4 |
--------------------------------------------------------------------------------
/data-jokesapi/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/data-jokesapi/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: "$rootDir/base-android-library.gradle"
2 |
3 | dependencies {
4 | def applicationDependencies = rootProject.ext.mainApplication
5 |
6 | implementation applicationDependencies.retrofit
7 | implementation applicationDependencies.retrofitConverterGson
8 | implementation applicationDependencies.retrofitRxjava2Adapter
9 | implementation applicationDependencies.okHttpLoggingInterceptor
10 | }
11 |
--------------------------------------------------------------------------------
/data-jokesapi/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/data-jokesapi/consumer-rules.pro
--------------------------------------------------------------------------------
/data-jokesapi/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/data-jokesapi/src/androidTest/java/prieto/fernando/data_jokesapi/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.data_jokesapi
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("prieto.fernando.data_jokesapi.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/data-jokesapi/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/data-jokesapi/src/main/java/prieto/fernando/ApiService.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando
2 |
3 | import io.reactivex.Single
4 | import prieto.fernando.model.MultipleRandomJokeResponse
5 | import prieto.fernando.model.RandomJokeResponse
6 | import retrofit2.http.GET
7 | import retrofit2.http.Path
8 | import retrofit2.http.Query
9 |
10 | interface ApiService {
11 |
12 | @GET("jokes/random")
13 | fun getRandomJoke(): Single
14 |
15 | @GET("jokes/random")
16 | fun getRandomCustomJoke(
17 | @Query("firstName") firstName: String,
18 | @Query("lastName") lastName: String
19 | ): Single
20 |
21 | @GET("jokes/random/{numberOfJokes}")
22 | fun getMultipleRandomJoke(@Path("numberOfJokes") numberOfJokes: Int):
23 | Single
24 | }
25 |
--------------------------------------------------------------------------------
/data-jokesapi/src/main/java/prieto/fernando/data/JokesRemoteSource.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.data
2 |
3 | import prieto.fernando.ApiService
4 | import javax.inject.Inject
5 | import javax.inject.Singleton
6 |
7 | @Singleton
8 | class JokesRemoteSource
9 | @Inject constructor(
10 | private val apiService: ApiService
11 | ) {
12 | fun getRandomJoke() = apiService.getRandomJoke()
13 |
14 | fun getRandomCustomJoke(firstName: String, lastName: String) =
15 | apiService.getRandomCustomJoke(firstName, lastName)
16 |
17 | fun getMultipleRandomJoke(numberOfJokes: Int) =
18 | apiService.getMultipleRandomJoke(numberOfJokes)
19 | }
20 |
--------------------------------------------------------------------------------
/data-jokesapi/src/main/java/prieto/fernando/di/BaseUrl.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.di
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Qualifier
6 | @Retention(AnnotationRetention.RUNTIME)
7 | @Target(
8 | AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER,
9 | AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
10 | )
11 | annotation class BaseUrl
--------------------------------------------------------------------------------
/data-jokesapi/src/main/java/prieto/fernando/di/ChuckNorrisApiModule.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import javax.inject.Singleton
6 | import okhttp3.OkHttpClient
7 | import prieto.fernando.ApiService
8 | import retrofit2.Retrofit
9 |
10 | @Module
11 | class ChuckNorrisApiModule {
12 |
13 | @Module
14 | companion object {
15 | @Provides
16 | @JvmStatic
17 | @Singleton
18 | internal fun provideChuckNorrisApi(retrofit: Retrofit) =
19 | retrofit.create(ApiService::class.java)
20 |
21 | @Provides
22 | @JvmStatic
23 | @Singleton
24 | internal fun provideChuckNorrisRetrofit(
25 | httpBuilder: OkHttpClient.Builder,
26 | retrofitBuilder: Retrofit.Builder
27 | ) = retrofitBuilder
28 | .client(httpBuilder.build())
29 | .build()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/data-jokesapi/src/main/java/prieto/fernando/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.di
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import com.google.gson.Gson
6 | import com.google.gson.GsonBuilder
7 | import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
8 | import dagger.Module
9 | import dagger.Provides
10 | import okhttp3.OkHttpClient
11 | import okhttp3.logging.HttpLoggingInterceptor
12 | import prieto.fernando.data_jokesapi.BuildConfig
13 | import retrofit2.Retrofit
14 | import retrofit2.converter.gson.GsonConverterFactory
15 | import java.util.concurrent.TimeUnit
16 | import javax.inject.Named
17 | import javax.inject.Singleton
18 |
19 | @Module
20 | class NetworkModule(
21 | private val baseUrlOverride: String? = null
22 | ) {
23 | @Provides
24 | @BaseUrl
25 | fun provideBaseUrl() = baseUrlOverride ?: "https://api.icndb.com/"
26 |
27 | @Provides
28 | @Singleton
29 | fun provideRetrofitBuilder(
30 | rxJavaCallAdapterFactory: RxJava2CallAdapterFactory,
31 | gsonConverterFactory: GsonConverterFactory,
32 | @BaseUrl baseUrl: String
33 | ) = Retrofit.Builder()
34 | .baseUrl(baseUrl)
35 | .addConverterFactory(gsonConverterFactory)
36 | .addCallAdapterFactory(rxJavaCallAdapterFactory)
37 |
38 | @Provides
39 | @Singleton
40 | fun provideHttpBuilder() =
41 | OkHttpClient.Builder().apply {
42 | if (BuildConfig.DEBUG) {
43 | val httpLoggingInterceptor = HttpLoggingInterceptor()
44 | httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
45 | addInterceptor(httpLoggingInterceptor)
46 | }
47 |
48 | readTimeout(RETROFIT_TIMEOUT, TimeUnit.SECONDS)
49 | connectTimeout(RETROFIT_TIMEOUT, TimeUnit.SECONDS)
50 | }
51 |
52 | @Provides
53 | @Singleton
54 | fun provideGson(): Gson = GsonBuilder().create()
55 |
56 | @Provides
57 | @Singleton
58 | fun provideGsonConverterFactory(
59 | gson: Gson
60 | ): GsonConverterFactory = GsonConverterFactory.create(gson)
61 |
62 | @Provides
63 | @Singleton
64 | fun provideRxJavaCallAdapter(): RxJava2CallAdapterFactory = RxJava2CallAdapterFactory.create()
65 |
66 | @Provides
67 | @Singleton
68 | fun provideConnectivityManager(context: Context): ConnectivityManager =
69 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
70 |
71 | companion object {
72 | const val RETROFIT_TIMEOUT = 60L
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/data-jokesapi/src/main/java/prieto/fernando/model/MultipleRandomJokeResponse.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.model
2 |
3 | data class MultipleRandomJokeResponse(
4 | val type: String,
5 | val value: List
6 | )
7 |
8 | data class MultipleJokeResponse(
9 | val id: String,
10 | val joke: String,
11 | val categories: List
12 | )
13 |
--------------------------------------------------------------------------------
/data-jokesapi/src/main/java/prieto/fernando/model/RandomJokeResponse.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.model
2 |
3 | data class RandomJokeResponse(
4 | val type: String,
5 | val value: JokeResponse
6 | )
7 |
8 | data class JokeResponse(
9 | val id: String,
10 | val joke: String,
11 | val categories: List
12 | )
13 |
--------------------------------------------------------------------------------
/data-jokesapi/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | data-jokesapi
3 |
4 |
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: "$rootDir/base-android-library.gradle"
2 |
3 | dependencies {
4 | implementation project(':data-jokesapi')
5 | implementation project(':data-cache')
6 | }
7 |
--------------------------------------------------------------------------------
/domain/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/domain/consumer-rules.pro
--------------------------------------------------------------------------------
/domain/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/domain/src/androidTest/java/prieto/fernando/domain/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.domain
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("prieto.fernando.domain.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/domain/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/domain/src/main/java/prieto/fernando/data/RandomJokeDomainModel.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.data
2 |
3 | data class RandomJokeDomainModel(
4 | val id: String,
5 | val joke: String,
6 | val categories: List
7 | )
8 |
9 | enum class CategoryDomainModel {
10 | EXPLICIT,
11 | NERDY,
12 | UNKNOWN
13 | }
14 |
--------------------------------------------------------------------------------
/domain/src/main/java/prieto/fernando/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import prieto.fernando.data.JokesRemoteSource
6 | import prieto.fernando.mapper.MultipleRandomJokeResponseToLocalModelMapper
7 | import prieto.fernando.mapper.RandomJokeResponseToLocalModelMapper
8 | import prieto.fernando.repository.JokesRepositoryImpl
9 | import prieto.fernando.source.JokesLocalSource
10 |
11 | @Module
12 | class RepositoryModule {
13 |
14 | @Module
15 | companion object {
16 | @Provides
17 | fun provideRandomJokeResponseToLocalModelMapperRepository(
18 | localSource: JokesLocalSource,
19 | remoteSource: JokesRemoteSource,
20 | jokeResponseToLocalMapper: RandomJokeResponseToLocalModelMapper,
21 | multipleJokesResponseToLocalMapper: MultipleRandomJokeResponseToLocalModelMapper
22 | ) = JokesRepositoryImpl(
23 | localSource,
24 | remoteSource,
25 | jokeResponseToLocalMapper,
26 | multipleJokesResponseToLocalMapper
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/domain/src/main/java/prieto/fernando/mapper/MultipleRandomJokeResponseToLocalModelMapper.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.mapper
2 |
3 | import dagger.Reusable
4 | import javax.inject.Inject
5 | import prieto.fernando.model.CategoryLocalModel
6 | import prieto.fernando.model.RandomJokeLocalModel
7 | import prieto.fernando.model.MultipleRandomJokeResponse
8 |
9 | @Reusable
10 | class MultipleRandomJokeResponseToLocalModelMapper @Inject constructor() {
11 | fun toLocal(multipleRandomJokeResponse: MultipleRandomJokeResponse) =
12 | multipleRandomJokeResponse.value.map { multipleJokeResponse ->
13 | RandomJokeLocalModel(
14 | id = multipleJokeResponse.id,
15 | joke = multipleJokeResponse.joke,
16 | categories = getLocalCategories(multipleJokeResponse.categories)
17 | )
18 | }
19 |
20 | private fun getLocalCategories(categories: List) =
21 | categories.map { category ->
22 | when (category) {
23 | "explicit" -> CategoryLocalModel.EXPLICIT
24 | "nerdy" -> CategoryLocalModel.NERDY
25 | else -> CategoryLocalModel.UNKNOWN
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/domain/src/main/java/prieto/fernando/mapper/RandomJokeLocalToDomainModelMapper.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.mapper
2 |
3 | import dagger.Reusable
4 | import javax.inject.Inject
5 | import prieto.fernando.data.CategoryDomainModel
6 | import prieto.fernando.model.CategoryLocalModel
7 | import prieto.fernando.data.RandomJokeDomainModel
8 | import prieto.fernando.model.RandomJokeLocalModel
9 |
10 | @Reusable
11 | class RandomJokeLocalToDomainModelMapper @Inject constructor() {
12 | fun toDomain(randomJokeResponse: RandomJokeLocalModel) =
13 | RandomJokeDomainModel(
14 | id = randomJokeResponse.id,
15 | joke = randomJokeResponse.joke,
16 | categories = getDomainCategories(randomJokeResponse.categories)
17 | )
18 |
19 | private fun getDomainCategories(categories: List) =
20 | categories.map { category ->
21 | when (category) {
22 | CategoryLocalModel.EXPLICIT -> CategoryDomainModel.EXPLICIT
23 | CategoryLocalModel.NERDY -> CategoryDomainModel.NERDY
24 | else -> CategoryDomainModel.UNKNOWN
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/domain/src/main/java/prieto/fernando/mapper/RandomJokeResponseToLocalModelMapper.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.mapper
2 |
3 | import dagger.Reusable
4 | import javax.inject.Inject
5 | import prieto.fernando.model.CategoryLocalModel
6 | import prieto.fernando.model.RandomJokeLocalModel
7 | import prieto.fernando.model.RandomJokeResponse
8 |
9 | @Reusable
10 | class RandomJokeResponseToLocalModelMapper @Inject constructor() {
11 | fun toLocal(randomJokeResponse: RandomJokeResponse) =
12 | RandomJokeLocalModel(
13 | id = randomJokeResponse.value.id,
14 | joke = randomJokeResponse.value.joke,
15 | categories = getLocalCategories(randomJokeResponse.value.categories)
16 | )
17 |
18 | private fun getLocalCategories(categories: List) =
19 | categories.map { category ->
20 | when (category) {
21 | "explicit" -> CategoryLocalModel.EXPLICIT
22 | "nerdy" -> CategoryLocalModel.NERDY
23 | else -> CategoryLocalModel.UNKNOWN
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/domain/src/main/java/prieto/fernando/repository/JokesRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.repository
2 |
3 | import io.reactivex.Single
4 | import prieto.fernando.mapper.MultipleRandomJokeResponseToLocalModelMapper
5 | import prieto.fernando.mapper.RandomJokeResponseToLocalModelMapper
6 | import javax.inject.Inject
7 | import prieto.fernando.source.JokesLocalSource
8 | import prieto.fernando.data.JokesRemoteSource
9 | import prieto.fernando.model.RandomJokeLocalModel
10 |
11 | interface JokesRepository {
12 | fun randomJoke(): Single
13 |
14 | fun randomCustomJoke(
15 | firstName: String?,
16 | lastName: String?
17 | ): Single
18 |
19 | fun multipleRandomJokes(
20 | numberOfJokes: Int
21 | ): Single>
22 |
23 | fun resetCustomRandomJoke(): Single
24 | }
25 |
26 | class JokesRepositoryImpl @Inject constructor(
27 | private val localSource: JokesLocalSource,
28 | private val remoteSource: JokesRemoteSource,
29 | private val jokeResponseToLocalMapper: RandomJokeResponseToLocalModelMapper,
30 | private val multipleJokeResponseToLocalMapper: MultipleRandomJokeResponseToLocalModelMapper
31 | ) : JokesRepository {
32 | override fun randomJoke(): Single = remoteSource.getRandomJoke().map { response ->
33 | jokeResponseToLocalMapper.toLocal(response)
34 | }
35 |
36 | override fun randomCustomJoke(
37 | firstName: String?,
38 | lastName: String?
39 | ): Single = if (firstName.isNullOrBlank() || lastName.isNullOrBlank()) {
40 | retrieveLocalJokeOrException()
41 | } else {
42 | remoteSource.getRandomCustomJoke(firstName, lastName).map { response ->
43 | val randomJokeLocalModel = jokeResponseToLocalMapper.toLocal(response)
44 | localSource.setCustomJoke(randomJokeLocalModel)
45 | randomJokeLocalModel
46 | }
47 | }
48 |
49 | override fun multipleRandomJokes(
50 | numberOfJokes: Int
51 | ): Single> = remoteSource.getMultipleRandomJoke(numberOfJokes).map { response ->
52 | multipleJokeResponseToLocalMapper.toLocal(response)
53 | }
54 |
55 | override fun resetCustomRandomJoke(): Single = localSource.resetData()
56 |
57 | private fun retrieveLocalJokeOrException() =
58 | if (localSource.hasCustomJokesValidData()) {
59 | localSource.getCustomJoke()
60 | } else {
61 | Single.error(CustomJokesException())
62 | }
63 | }
64 |
65 | class CustomJokesException : Exception("There are no jokes saved in cache")
66 |
--------------------------------------------------------------------------------
/domain/src/main/java/prieto/fernando/usecase/GetCustomRandomJokeUseCase.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.usecase
2 |
3 | import dagger.Reusable
4 | import prieto.fernando.repository.JokesRepositoryImpl
5 | import prieto.fernando.mapper.RandomJokeLocalToDomainModelMapper
6 | import javax.inject.Inject
7 |
8 | @Reusable
9 | class GetCustomRandomJokeUseCase @Inject constructor(
10 | private val jokesRepository: JokesRepositoryImpl,
11 | private val randomJokeToDomainMapper: RandomJokeLocalToDomainModelMapper
12 | ) {
13 | fun execute(firstName: String?, lastName: String?) =
14 | jokesRepository.randomCustomJoke(firstName, lastName)
15 | .map { randomJokeLocalModel ->
16 | randomJokeToDomainMapper.toDomain(randomJokeLocalModel)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/domain/src/main/java/prieto/fernando/usecase/GetMultipleRandomJokeUseCase.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.usecase
2 |
3 | import dagger.Reusable
4 | import io.reactivex.Single
5 | import prieto.fernando.repository.JokesRepositoryImpl
6 | import prieto.fernando.mapper.RandomJokeLocalToDomainModelMapper
7 | import javax.inject.Inject
8 |
9 | @Reusable
10 | class GetMultipleRandomJokeUseCase @Inject constructor(
11 | private val jokesRepository: JokesRepositoryImpl,
12 | private val randomJokeToDomainMapper: RandomJokeLocalToDomainModelMapper
13 | ) {
14 | fun execute(numberOfJokes: Int) = jokesRepository.multipleRandomJokes(numberOfJokes)
15 | .flatMap { randomJokeLocalModels ->
16 | Single.just(
17 | randomJokeLocalModels.map { randomJokeLocalModel ->
18 | randomJokeToDomainMapper.toDomain(
19 | randomJokeLocalModel
20 | )
21 | }
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/domain/src/main/java/prieto/fernando/usecase/GetRandomJokeUseCase.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.usecase
2 |
3 | import dagger.Reusable
4 | import io.reactivex.Single
5 | import prieto.fernando.repository.JokesRepositoryImpl
6 | import prieto.fernando.mapper.RandomJokeLocalToDomainModelMapper
7 | import javax.inject.Inject
8 |
9 | @Reusable
10 | class GetRandomJokeUseCase @Inject constructor(
11 | private val jokesRepository: JokesRepositoryImpl,
12 | private val randomJokeToDomainMapper: RandomJokeLocalToDomainModelMapper
13 | ) {
14 | fun execute() = jokesRepository.randomJoke()
15 | .flatMap { randomJokeLocalModel ->
16 | Single.just(
17 | randomJokeToDomainMapper.toDomain(
18 | randomJokeLocalModel
19 | )
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/domain/src/main/java/prieto/fernando/usecase/ResetCustomRandomJokeUseCase.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.usecase
2 |
3 | import dagger.Reusable
4 | import prieto.fernando.repository.JokesRepositoryImpl
5 | import javax.inject.Inject
6 |
7 | @Reusable
8 | class ResetCustomRandomJokeUseCase @Inject constructor(
9 | private val jokesRepository: JokesRepositoryImpl
10 | ) {
11 | fun execute() = jokesRepository.resetCustomRandomJoke()
12 | }
13 |
--------------------------------------------------------------------------------
/domain/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | domain
3 |
4 |
--------------------------------------------------------------------------------
/domain/src/test/java/prieto/fernando/domain/mapper/MultipleRandomJokeResponseToLocalModelMapperTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.domain.mapper
2 |
3 | import junit.framework.Assert.assertEquals
4 | import org.junit.Before
5 | import org.junit.Test
6 | import prieto.fernando.mapper.MultipleRandomJokeResponseToLocalModelMapper
7 | import prieto.fernando.model.CategoryLocalModel
8 | import prieto.fernando.model.MultipleJokeResponse
9 | import prieto.fernando.model.MultipleRandomJokeResponse
10 | import prieto.fernando.model.RandomJokeLocalModel
11 |
12 | class MultipleRandomJokeResponseToLocalModelMapperTest {
13 | private lateinit var cut: MultipleRandomJokeResponseToLocalModelMapper
14 |
15 | @Before
16 | fun setUp() {
17 | cut = MultipleRandomJokeResponseToLocalModelMapper()
18 | }
19 |
20 | @Test
21 | fun `Given MultipleRandomJokeResponse when toLocal then return list of RandomJokeLocalModel`() {
22 | // Given
23 | val type = "some Type"
24 | val idFirst = "Id0"
25 | val idSecond = "Id1"
26 | val joke = "nice joke"
27 | val categoryResponseModels = listOf("explicit", "nerdy")
28 | val multipleRandomJokeResponse = MultipleRandomJokeResponse(
29 | type,
30 | listOf(
31 | MultipleJokeResponse(
32 | idFirst,
33 | joke,
34 | categoryResponseModels
35 | ),
36 | MultipleJokeResponse(
37 | idSecond,
38 | joke,
39 | categoryResponseModels
40 | )
41 | )
42 | )
43 |
44 | val categoryLocalModels = listOf(CategoryLocalModel.EXPLICIT, CategoryLocalModel.NERDY)
45 | val expected = listOf(
46 | RandomJokeLocalModel(
47 | idFirst,
48 | joke,
49 | categoryLocalModels
50 | ),
51 | RandomJokeLocalModel(
52 | idSecond,
53 | joke,
54 | categoryLocalModels
55 | )
56 | )
57 |
58 | // When
59 | val actualValue = cut.toLocal(multipleRandomJokeResponse)
60 |
61 | // Then
62 | assertEquals(expected, actualValue)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/domain/src/test/java/prieto/fernando/domain/mapper/RandomJokeLocalToDomainModelMapperTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.domain.mapper
2 |
3 | import junit.framework.Assert.assertEquals
4 | import org.junit.Before
5 | import org.junit.Test
6 | import prieto.fernando.data.CategoryDomainModel
7 | import prieto.fernando.data.RandomJokeDomainModel
8 | import prieto.fernando.mapper.RandomJokeLocalToDomainModelMapper
9 | import prieto.fernando.model.CategoryLocalModel
10 | import prieto.fernando.model.RandomJokeLocalModel
11 |
12 | class RandomJokeLocalToDomainModelMapperTest {
13 | private lateinit var cut: RandomJokeLocalToDomainModelMapper
14 |
15 | @Before
16 | fun setUp() {
17 | cut = RandomJokeLocalToDomainModelMapper()
18 | }
19 |
20 | @Test
21 | fun `Given RandomJokeLocalModel when toDomain then return expected RandomJokeDomainModel`() {
22 | // Given
23 | val type = "some Type"
24 | val id = "some Id"
25 | val joke = "nice joke"
26 | val categoryLocalModels = listOf(CategoryLocalModel.EXPLICIT, CategoryLocalModel.NERDY)
27 | val randomJokeLocalModel = RandomJokeLocalModel(
28 | id,
29 | joke,
30 | categoryLocalModels
31 | )
32 | val categoryDomainModels = listOf(CategoryDomainModel.EXPLICIT, CategoryDomainModel.NERDY)
33 | val expected = RandomJokeDomainModel(
34 | id,
35 | joke,
36 | categoryDomainModels
37 | )
38 |
39 | // When
40 | val actualValue = cut.toDomain(randomJokeLocalModel)
41 |
42 | // Then
43 | assertEquals(expected, actualValue)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/domain/src/test/java/prieto/fernando/domain/mapper/RandomJokeResponseToLocalModelMapperTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.domain.mapper
2 |
3 | import junit.framework.Assert.assertEquals
4 | import org.junit.Before
5 | import org.junit.Test
6 | import prieto.fernando.mapper.RandomJokeResponseToLocalModelMapper
7 | import prieto.fernando.model.CategoryLocalModel
8 | import prieto.fernando.model.JokeResponse
9 | import prieto.fernando.model.RandomJokeLocalModel
10 | import prieto.fernando.model.RandomJokeResponse
11 |
12 | class RandomJokeResponseToLocalModelMapperTest {
13 | private lateinit var cut: RandomJokeResponseToLocalModelMapper
14 |
15 | @Before
16 | fun setUp() {
17 | cut = RandomJokeResponseToLocalModelMapper()
18 | }
19 |
20 | @Test
21 | fun `Given RandomJokeResponse when toLocal then expected result`() {
22 | // Given
23 | val type = "some Type"
24 | val id = "some Id"
25 | val joke = "nice joke"
26 | val categoryResponseModels = listOf("explicit", "nerdy")
27 | val categoryLocalModels = listOf(CategoryLocalModel.EXPLICIT, CategoryLocalModel.NERDY)
28 | val randomJokeResponse = RandomJokeResponse(
29 | type,
30 | JokeResponse(
31 | id,
32 | joke,
33 | categoryResponseModels
34 | )
35 | )
36 | val expected = RandomJokeLocalModel(
37 | id,
38 | joke,
39 | categoryLocalModels
40 | )
41 |
42 | // When
43 | val actualValue = cut.toLocal(randomJokeResponse)
44 |
45 | // Then
46 | assertEquals(expected, actualValue)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/domain/src/test/java/prieto/fernando/domain/repository/JokesRepositoryImplTest.kt:
--------------------------------------------------------------------------------
1 | package domain.repository
2 |
3 | import com.nhaarman.mockito_kotlin.given
4 | import io.reactivex.Single
5 | import org.junit.Before
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.mockito.Mock
9 | import org.mockito.junit.MockitoJUnitRunner
10 | import prieto.fernando.data.JokesRemoteSource
11 | import prieto.fernando.mapper.MultipleRandomJokeResponseToLocalModelMapper
12 | import prieto.fernando.mapper.RandomJokeResponseToLocalModelMapper
13 | import prieto.fernando.model.CategoryLocalModel
14 | import prieto.fernando.model.JokeResponse
15 | import prieto.fernando.model.RandomJokeLocalModel
16 | import prieto.fernando.model.RandomJokeResponse
17 | import prieto.fernando.repository.JokesRepositoryImpl
18 | import prieto.fernando.source.JokesLocalSource
19 |
20 | @RunWith(MockitoJUnitRunner::class)
21 | class JokesRepositoryImplTest {
22 | private lateinit var cut: JokesRepositoryImpl
23 |
24 | @Mock
25 | lateinit var localSource: JokesLocalSource
26 |
27 | @Mock
28 | lateinit var remoteSource: JokesRemoteSource
29 |
30 | @Mock
31 | lateinit var jokeResponseToLocalMapper: RandomJokeResponseToLocalModelMapper
32 |
33 | @Mock
34 | lateinit var multipleJokesResponseToLocalMapper: MultipleRandomJokeResponseToLocalModelMapper
35 |
36 | @Before
37 | fun setUp() {
38 | cut = JokesRepositoryImpl(
39 | localSource,
40 | remoteSource,
41 | jokeResponseToLocalMapper,
42 | multipleJokesResponseToLocalMapper
43 | )
44 | }
45 |
46 | @Test
47 | fun `When randomJoke then return RandomJokeLocalModel`() {
48 | // Given
49 | val randomJokeResponse = RandomJokeResponse(
50 | "some Type",
51 | JokeResponse(
52 | "some Id",
53 | "nice joke",
54 | listOf("explicit")
55 | )
56 | )
57 |
58 | val randomJokeLocalModel = RandomJokeLocalModel(
59 | "some Id",
60 | "nice joke",
61 | listOf(CategoryLocalModel.EXPLICIT)
62 | )
63 |
64 | given { remoteSource.getRandomJoke() }.willReturn(
65 | Single.just(randomJokeResponse)
66 | )
67 | given { jokeResponseToLocalMapper.toLocal(randomJokeResponse) }.willReturn(
68 | randomJokeLocalModel
69 | )
70 |
71 | // When
72 | val actualValue = cut.randomJoke()
73 |
74 | // Then
75 | actualValue
76 | .test()
77 | .assertValue(randomJokeLocalModel)
78 | .assertNoErrors()
79 | .assertComplete()
80 | }
81 |
82 | @Test
83 | fun `Given input parameters when randomCustomJoke then return RandomJokeLocalModel`() {
84 | // Given
85 | val randomJokeResponse = RandomJokeResponse(
86 | "some Type",
87 | JokeResponse(
88 | "some Id",
89 | "nice joke mentioning firstName lastName",
90 | listOf("explicit")
91 | )
92 | )
93 |
94 | val randomJokeLocalModel = RandomJokeLocalModel(
95 | "some Id",
96 | "nice joke mentioning firstName lastName",
97 | listOf(CategoryLocalModel.EXPLICIT)
98 | )
99 | given { remoteSource.getRandomCustomJoke("firstName", "lastName") }.willReturn(
100 | Single.just(randomJokeResponse)
101 | )
102 | given { jokeResponseToLocalMapper.toLocal(randomJokeResponse) }.willReturn(
103 | randomJokeLocalModel
104 | )
105 |
106 | // When
107 | val actualValue = cut.randomCustomJoke("firstName", "lastName")
108 |
109 | // Then
110 | actualValue
111 | .test()
112 | .assertValue(randomJokeLocalModel)
113 | .assertNoErrors()
114 | .assertComplete()
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/domain/src/test/java/prieto/fernando/domain/usecase/GetCustomRandomJokeUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.domain.usecase
2 |
3 | import com.nhaarman.mockito_kotlin.whenever
4 | import io.reactivex.Single
5 | import java.lang.Exception
6 | import org.junit.Before
7 | import org.junit.Test
8 | import org.junit.runner.RunWith
9 | import org.mockito.Mock
10 | import org.mockito.junit.MockitoJUnitRunner
11 | import prieto.fernando.data.CategoryDomainModel
12 | import prieto.fernando.data.RandomJokeDomainModel
13 | import prieto.fernando.mapper.RandomJokeLocalToDomainModelMapper
14 | import prieto.fernando.model.CategoryLocalModel
15 | import prieto.fernando.model.RandomJokeLocalModel
16 | import prieto.fernando.repository.JokesRepositoryImpl
17 | import prieto.fernando.usecase.GetCustomRandomJokeUseCase
18 |
19 | @RunWith(MockitoJUnitRunner::class)
20 | class GetCustomRandomJokeUseCaseTest {
21 | private lateinit var cut: GetCustomRandomJokeUseCase
22 |
23 | @Mock
24 | lateinit var jokesRepository: JokesRepositoryImpl
25 |
26 | @Mock
27 | lateinit var randomJokeToDomainMapper: RandomJokeLocalToDomainModelMapper
28 |
29 | @Before
30 | fun setUp() {
31 | cut = GetCustomRandomJokeUseCase(jokesRepository, randomJokeToDomainMapper)
32 | }
33 |
34 | @Test
35 | fun `When execute then returns expected RandomJokeLocalModel`() {
36 | // Given
37 | val randomJokeLocalModel = RandomJokeLocalModel(
38 | "some Id",
39 | "nice joke FirstName LastName",
40 | listOf(CategoryLocalModel.EXPLICIT)
41 | )
42 | val randomJokeDomainModel = RandomJokeDomainModel(
43 | "some Id",
44 | "nice joke FirstName LastName",
45 | listOf(CategoryDomainModel.EXPLICIT)
46 | )
47 | whenever(jokesRepository.randomCustomJoke("FirstName", "LastName"))
48 | .thenReturn(Single.just(randomJokeLocalModel))
49 |
50 | whenever(randomJokeToDomainMapper.toDomain(randomJokeLocalModel))
51 | .thenReturn(randomJokeDomainModel)
52 |
53 | // When
54 | val actualValue = cut.execute("FirstName", "LastName")
55 |
56 | // Then
57 | actualValue.test()
58 | .assertResult(randomJokeDomainModel)
59 | .assertComplete()
60 | .assertNoErrors()
61 | }
62 |
63 | @Test
64 | fun `Given no names When execute then returns expected RandomJokeLocalMode from cache`() {
65 | // Given
66 | val randomJokeLocalModel = RandomJokeLocalModel(
67 | "some Id",
68 | "nice joke FirstName LastName",
69 | listOf(CategoryLocalModel.EXPLICIT)
70 | )
71 | val randomJokeDomainModel = RandomJokeDomainModel(
72 | "some Id",
73 | "nice joke FirstName LastName",
74 | listOf(CategoryDomainModel.EXPLICIT)
75 | )
76 | whenever(jokesRepository.randomCustomJoke(null, null))
77 | .thenReturn(Single.just(randomJokeLocalModel))
78 |
79 | whenever(randomJokeToDomainMapper.toDomain(randomJokeLocalModel))
80 | .thenReturn(randomJokeDomainModel)
81 |
82 | // When
83 | val actualValue = cut.execute(null, null)
84 |
85 | // Then
86 | actualValue.test()
87 | .assertResult(randomJokeDomainModel)
88 | .assertComplete()
89 | .assertNoErrors()
90 | }
91 |
92 | @Test(expected = Exception::class)
93 | fun `Given no names When execute then returns exception`() {
94 | // Given
95 | whenever(jokesRepository.randomCustomJoke(null, null))
96 | .thenThrow()
97 |
98 | // When
99 | cut.execute(null, null)
100 |
101 | // Then
102 | // Throws CustomJokesException
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/domain/src/test/java/prieto/fernando/domain/usecase/GetMultipleRandomJokeUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.domain.usecase
2 |
3 | import com.nhaarman.mockito_kotlin.whenever
4 | import io.reactivex.Single
5 | import org.junit.Before
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.mockito.Mock
9 | import org.mockito.junit.MockitoJUnitRunner
10 | import prieto.fernando.data.CategoryDomainModel
11 | import prieto.fernando.data.RandomJokeDomainModel
12 | import prieto.fernando.mapper.RandomJokeLocalToDomainModelMapper
13 | import prieto.fernando.model.CategoryLocalModel
14 | import prieto.fernando.model.RandomJokeLocalModel
15 | import prieto.fernando.repository.JokesRepositoryImpl
16 | import prieto.fernando.usecase.GetMultipleRandomJokeUseCase
17 |
18 | @RunWith(MockitoJUnitRunner::class)
19 | class GetMultipleRandomJokeUseCaseTest {
20 | private lateinit var cut: GetMultipleRandomJokeUseCase
21 |
22 | @Mock
23 | lateinit var jokesRepository: JokesRepositoryImpl
24 |
25 | @Mock
26 | lateinit var randomJokeToDomainMapper: RandomJokeLocalToDomainModelMapper
27 |
28 | @Before
29 | fun setUp() {
30 | cut =
31 | GetMultipleRandomJokeUseCase(jokesRepository, randomJokeToDomainMapper)
32 | }
33 |
34 | @Test
35 | fun `Given numberOfJokes when execute then return list of RandomJokeDomainModel`() {
36 | // Given
37 | val numberOfJokes = 2
38 | val randomJokeLocalModelFirst = RandomJokeLocalModel(
39 | "Id0",
40 | "nice joke",
41 | listOf(CategoryLocalModel.EXPLICIT)
42 | )
43 | val randomJokeLocalModelSecond = RandomJokeLocalModel(
44 | "Id1",
45 | "nice joke",
46 | listOf(CategoryLocalModel.NERDY)
47 | )
48 | val randomJokeDomainModelFirst = RandomJokeDomainModel(
49 | "Id0",
50 | "nice joke",
51 | listOf(CategoryDomainModel.EXPLICIT)
52 | )
53 | val randomJokeDomainModelSecond = RandomJokeDomainModel(
54 | "Id1",
55 | "nice joke",
56 | listOf(CategoryDomainModel.NERDY)
57 | )
58 |
59 | whenever(jokesRepository.multipleRandomJokes(numberOfJokes)).thenReturn(
60 | Single.just(listOf(randomJokeLocalModelFirst, randomJokeLocalModelSecond))
61 | )
62 | whenever(randomJokeToDomainMapper.toDomain(randomJokeLocalModelFirst))
63 | .thenReturn(randomJokeDomainModelFirst)
64 | whenever(randomJokeToDomainMapper.toDomain(randomJokeLocalModelSecond))
65 | .thenReturn(randomJokeDomainModelSecond)
66 |
67 | // When
68 | val actualValue = cut.execute(numberOfJokes)
69 |
70 | // Then
71 | actualValue.test()
72 | .assertResult(listOf(randomJokeDomainModelFirst, randomJokeDomainModelSecond))
73 | .assertComplete()
74 | .assertNoErrors()
75 | }
76 |
77 | @Test
78 | fun `Given 0 numberOfJokes when execute then return an empty list`() {
79 | // Given
80 | val numberOfJokes = 0
81 |
82 | whenever(jokesRepository.multipleRandomJokes(numberOfJokes)).thenReturn(
83 | Single.just(emptyList())
84 | )
85 |
86 | // When
87 | val actualValue = cut.execute(numberOfJokes)
88 |
89 | // Then
90 | actualValue.test()
91 | .assertResult(emptyList())
92 | .assertComplete()
93 | .assertNoErrors()
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/domain/src/test/java/prieto/fernando/domain/usecase/GetRandomJokeUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.domain.usecase
2 |
3 | import com.nhaarman.mockito_kotlin.whenever
4 | import io.reactivex.Single
5 | import org.junit.Before
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.mockito.Mock
9 | import org.mockito.junit.MockitoJUnitRunner
10 | import prieto.fernando.data.CategoryDomainModel
11 | import prieto.fernando.data.RandomJokeDomainModel
12 | import prieto.fernando.mapper.RandomJokeLocalToDomainModelMapper
13 | import prieto.fernando.model.CategoryLocalModel
14 | import prieto.fernando.model.RandomJokeLocalModel
15 | import prieto.fernando.repository.JokesRepositoryImpl
16 | import prieto.fernando.usecase.GetRandomJokeUseCase
17 |
18 | @RunWith(MockitoJUnitRunner::class)
19 | class GetRandomJokeUseCaseTest {
20 | private lateinit var cut: GetRandomJokeUseCase
21 |
22 | @Mock
23 | lateinit var jokesRepository: JokesRepositoryImpl
24 |
25 | @Mock
26 | lateinit var randomJokeToDomainMapper: RandomJokeLocalToDomainModelMapper
27 |
28 | @Before
29 | fun setUp() {
30 | cut = GetRandomJokeUseCase(jokesRepository, randomJokeToDomainMapper)
31 | }
32 |
33 | @Test
34 | fun `When execute then returns expected RandomJokeLocalModel`() {
35 | // Given
36 | val randomJokeLocalModel = RandomJokeLocalModel(
37 | "some Id",
38 | "nice joke",
39 | listOf(CategoryLocalModel.EXPLICIT)
40 | )
41 | val randomJokeDomainModel = RandomJokeDomainModel(
42 | "some Id",
43 | "nice joke",
44 | listOf(CategoryDomainModel.EXPLICIT)
45 | )
46 | whenever(jokesRepository.randomJoke()).thenReturn(
47 | Single.just(randomJokeLocalModel)
48 | )
49 | whenever(randomJokeToDomainMapper.toDomain(randomJokeLocalModel))
50 | .thenReturn(randomJokeDomainModel)
51 |
52 | // When
53 | val actualValue = cut.execute()
54 |
55 | // Then
56 | actualValue.test()
57 | .assertResult(randomJokeDomainModel)
58 | .assertComplete()
59 | .assertNoErrors()
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | org.gradle.parallel=true
23 | org.gradle.configureondemand=true
24 | kotlin.incremental=true
25 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 17 20:13:03 BST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/navigation/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: "$rootDir/base-android-library.gradle"
2 |
3 | apply plugin: "androidx.navigation.safeargs"
4 |
5 | dependencies {
6 | def applicationDependencies = rootProject.ext.mainApplication
7 |
8 | implementation applicationDependencies.archNavigationFragment
9 | implementation applicationDependencies.archNavigationUi
10 | }
11 |
--------------------------------------------------------------------------------
/navigation/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/navigation/consumer-rules.pro
--------------------------------------------------------------------------------
/navigation/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/navigation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/navigation/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
14 |
18 |
19 |
20 |
23 |
24 |
27 |
31 |
34 |
35 |
36 |
39 |
--------------------------------------------------------------------------------
/presentation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/presentation/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: "$rootDir/base-android-library.gradle"
2 |
3 | dependencies{
4 | def applicationDependencies = rootProject.ext.mainApplication
5 |
6 | implementation project(':domain')
7 | implementation project(':core')
8 | implementation project(':navigation')
9 |
10 | implementation applicationDependencies.timber
11 | }
--------------------------------------------------------------------------------
/presentation/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ferPrieto/MVVM-Modularized/aecbd0054faa15beb29aac1c694eff9db0179432/presentation/consumer-rules.pro
--------------------------------------------------------------------------------
/presentation/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/presentation/src/androidTest/java/prieto/fernando/presentation/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("prieto.fernando.presentation.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/presentation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/presentation/src/main/java/prieto/fernando/presentation/RandomJokeUiModel.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation
2 |
3 | data class RandomJokeUiModel(
4 | val id: String,
5 | val joke: String,
6 | val categories: List
7 | )
8 |
9 | enum class CategoryUiModel {
10 | EXPLICIT,
11 | NERDY,
12 | UNKNOWN
13 | }
14 |
--------------------------------------------------------------------------------
/presentation/src/main/java/prieto/fernando/presentation/custom/CustomJokeViewModel.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.custom
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import prieto.fernando.presentation.BaseViewModel
6 | import prieto.fernando.presentation.BaseViewModelInputs
7 | import prieto.fernando.presentation.R
8 | import prieto.fernando.presentation.custom.model.NamesData
9 | import prieto.fernando.usecase.GetCustomRandomJokeUseCase
10 | import timber.log.Timber
11 | import javax.inject.Inject
12 |
13 | interface CustomJokeViewModelInputs : BaseViewModelInputs {
14 | fun onNamesChanged(namesData: NamesData)
15 | fun customRandomJoke(firstName: String, lastName: String)
16 | }
17 |
18 | class CustomJokeViewModel @Inject constructor(
19 | private val customJokeUseCase: GetCustomRandomJokeUseCase,
20 | private val buttonStateEvaluator: NamesButtonStateEvaluator
21 | ) : BaseViewModel(),
22 | CustomJokeViewModelInputs {
23 |
24 | private val doneButtonEnabled = MutableLiveData()
25 | private val customRandomJokeRetrieved = MutableLiveData()
26 | private val errorResource = MutableLiveData()
27 |
28 | override val inputs: CustomJokeViewModelInputs
29 | get() = this
30 |
31 | fun doneButtonEnabled(): LiveData = doneButtonEnabled
32 |
33 | fun customRandomJokeRetrieved(): LiveData = customRandomJokeRetrieved
34 |
35 | fun errorResource(): LiveData = errorResource
36 |
37 | override fun customRandomJoke(firstName: String, lastName: String) {
38 | customJokeUseCase.execute(firstName, lastName)
39 | .compose(schedulerProvider.doOnIoObserveOnMainSingle())
40 | .subscribe({
41 | customRandomJokeRetrieved.postValue(Unit)
42 | }, { throwable ->
43 | Timber.d(throwable)
44 | errorResource.postValue(R.string.custom_joke_retrieving_error_generic)
45 | }).also { subscriptions.add(it) }
46 | }
47 |
48 | override fun onNamesChanged(namesData: NamesData) {
49 | val namesChanged = buttonStateEvaluator.shouldEnableButton(
50 | namesData.firstName,
51 | namesData.lastName
52 | )
53 | doneButtonEnabled.postValue(namesChanged)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/presentation/src/main/java/prieto/fernando/presentation/custom/NamesButtonStateEvaluator.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.custom
2 |
3 | import dagger.Reusable
4 | import java.util.regex.Pattern
5 | import javax.inject.Inject
6 |
7 | @Reusable
8 | class NamesButtonStateEvaluator @Inject constructor() {
9 |
10 | fun shouldEnableButton(
11 | firstName: String?,
12 | lastName: String?
13 | ): Boolean {
14 | val notEmpty =
15 | isNotNullOrBlank(firstName) && isNotNullOrBlank(lastName)
16 | val firstNameOnlyContainLetters =
17 | firstName?.let { NAME_REGEX.matcher(firstName).matches() } ?: false
18 | val lastNameOnlyContainLetters =
19 | lastName?.let { NAME_REGEX.matcher(lastName).matches() } ?: false
20 | val onlyContainLetters = firstNameOnlyContainLetters && lastNameOnlyContainLetters
21 | val rightLength = isInTheRange(firstName) && isInTheRange(lastName)
22 | return notEmpty &&
23 | onlyContainLetters &&
24 | rightLength
25 | }
26 |
27 | private fun isNotNullOrBlank(value: String?) = !value.isNullOrBlank()
28 |
29 | private fun isInTheRange(value: String?) =
30 | value?.length ?: 0 in NAME_MIN_LENGTH..NAME_MAX_LENGTH
31 |
32 | companion object {
33 | const val NAME_MAX_LENGTH = 25
34 | const val NAME_MIN_LENGTH = 3
35 | val NAME_REGEX: Pattern = Pattern.compile("[a-zA-Z ]+")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/presentation/src/main/java/prieto/fernando/presentation/dashboard/DashboardViewModel.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.dashboard
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import prieto.fernando.presentation.BaseViewModel
6 | import prieto.fernando.presentation.BaseViewModelInputs
7 | import prieto.fernando.presentation.R
8 | import prieto.fernando.presentation.data.RandomJokeAndTitleResource
9 | import prieto.fernando.presentation.mapper.RandomJokeDomainToUiModelMapper
10 | import prieto.fernando.repository.CustomJokesException
11 | import prieto.fernando.usecase.GetCustomRandomJokeUseCase
12 | import prieto.fernando.usecase.GetRandomJokeUseCase
13 | import prieto.fernando.usecase.ResetCustomRandomJokeUseCase
14 | import timber.log.Timber
15 | import javax.inject.Inject
16 |
17 | interface DashboardViewModelInputs : BaseViewModelInputs {
18 | fun randomJoke()
19 | fun onCustomRandomJokeClicked()
20 | fun customRandomJokeForDialog()
21 | fun onMultipleJokesClicked()
22 | fun resetCustomJokeCache()
23 | }
24 |
25 | class DashboardViewModel @Inject constructor(
26 | private val customRandomJokeUseCase: GetCustomRandomJokeUseCase,
27 | private val randomJokeUseCase: GetRandomJokeUseCase,
28 | private val resetCustomRandomJokeUseCase: ResetCustomRandomJokeUseCase,
29 | private val randomJokeDomainToUiModelMapper: RandomJokeDomainToUiModelMapper
30 | ) : BaseViewModel(),
31 | DashboardViewModelInputs {
32 |
33 | private val navigateToCustomJoke: MutableLiveData = MutableLiveData()
34 | private val navigateToInfiniteJokes: MutableLiveData = MutableLiveData()
35 | private val randomJokeRetrieved: MutableLiveData = MutableLiveData()
36 | private val customRandomJokeRetrieved: MutableLiveData =
37 | MutableLiveData()
38 | private val errorResource: MutableLiveData = MutableLiveData()
39 |
40 | override val inputs: DashboardViewModelInputs
41 | get() = this
42 |
43 | fun navigateToCustomJoke(): LiveData = navigateToCustomJoke
44 |
45 | fun navigateToInfiniteJokes(): LiveData = navigateToInfiniteJokes
46 |
47 | fun randomJokeRetrieved(): LiveData = randomJokeRetrieved
48 |
49 | fun customRandomJokeRetrieved(): LiveData =
50 | customRandomJokeRetrieved
51 |
52 | fun errorResource(): LiveData = errorResource
53 |
54 |
55 | override fun randomJoke() {
56 | randomJokeUseCase.execute()
57 | .compose(schedulerProvider.doOnIoObserveOnMainSingle())
58 | .subscribe({ randomJokeDomainModel ->
59 | val randomJokeUiModel = randomJokeDomainToUiModelMapper.toUi(randomJokeDomainModel)
60 | val randomJokeAndTitleResource =
61 | RandomJokeAndTitleResource(
62 | randomJokeUiModel,
63 | R.string.dashboard_dialog_title
64 | )
65 | randomJokeRetrieved.postValue(randomJokeAndTitleResource)
66 | }, { throwable ->
67 | Timber.d(throwable)
68 | errorResource.value = R.string.random_joke_retrieving_error_generic
69 | }).also { subscriptions.add(it) }
70 | }
71 |
72 | override fun resetCustomJokeCache() {
73 | resetCustomRandomJokeUseCase.execute()
74 | .compose(schedulerProvider.doOnIoObserveOnMainSingle())
75 | .subscribe({
76 | }, { throwable ->
77 | Timber.d(throwable)
78 | }).also { subscriptions.add(it) }
79 | }
80 |
81 | override fun onCustomRandomJokeClicked() {
82 | navigateToCustomJoke.postValue(Unit)
83 | }
84 |
85 | override fun onMultipleJokesClicked() {
86 | navigateToInfiniteJokes.postValue(Unit)
87 | }
88 |
89 | @Throws(CustomJokesException::class)
90 | override fun customRandomJokeForDialog() {
91 | customRandomJokeUseCase.execute(null, null)
92 | .compose(schedulerProvider.doOnIoObserveOnMainSingle())
93 | .subscribe({ randomJokeDomainModel ->
94 | val randomJokeUiModel = randomJokeDomainToUiModelMapper.toUi(randomJokeDomainModel)
95 | val randomJokeAndTitleResource =
96 | RandomJokeAndTitleResource(
97 | randomJokeUiModel,
98 | R.string.custom_joke_dialog_title
99 | )
100 | customRandomJokeRetrieved.postValue(randomJokeAndTitleResource)
101 | }, { throwable ->
102 | Timber.d(throwable)
103 | }).also { subscriptions.add(it) }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/presentation/src/main/java/prieto/fernando/presentation/data/RandomJokeAndTitleResource.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.data
2 |
3 | import prieto.fernando.presentation.RandomJokeUiModel
4 |
5 |
6 | data class RandomJokeAndTitleResource(
7 | val randomJokeUiModel: RandomJokeUiModel,
8 | val titleResource: Int
9 | )
10 |
--------------------------------------------------------------------------------
/presentation/src/main/java/prieto/fernando/presentation/infinite/InfiniteJokesViewModel.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.infinite
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import prieto.fernando.data.RandomJokeDomainModel
6 | import prieto.fernando.presentation.BaseViewModel
7 | import prieto.fernando.presentation.BaseViewModelInputs
8 | import prieto.fernando.presentation.R
9 | import prieto.fernando.presentation.RandomJokeUiModel
10 | import prieto.fernando.presentation.mapper.RandomJokeDomainToUiModelMapper
11 | import prieto.fernando.usecase.GetMultipleRandomJokeUseCase
12 | import timber.log.Timber
13 | import javax.inject.Inject
14 |
15 | interface InfiniteJokeViewModelInputs : BaseViewModelInputs {
16 | fun multipleRandomJokes()
17 | fun onJokeSelected(joke: String)
18 | }
19 |
20 | private const val JOKES_REQUESTED = 12
21 |
22 | class InfiniteJokesViewModel @Inject constructor(
23 | private val multipleRandomJokeUseCase: GetMultipleRandomJokeUseCase,
24 | private val randomJokeDomainToUiModelMapper: RandomJokeDomainToUiModelMapper
25 | ) : BaseViewModel(), InfiniteJokeViewModelInputs {
26 |
27 | override val inputs: InfiniteJokeViewModelInputs
28 | get() = this
29 |
30 | private val multipleRandomJokesRetrieved: MutableLiveData> =
31 | MutableLiveData()
32 | private val errorResource: MutableLiveData = MutableLiveData()
33 | private val loading: MutableLiveData = MutableLiveData()
34 | private val jokeSelected: MutableLiveData = MutableLiveData()
35 |
36 | fun multipleRandomJokesRetrieved(): LiveData> =
37 | multipleRandomJokesRetrieved
38 |
39 | fun errorResource(): LiveData = errorResource
40 | fun loading(): LiveData = loading
41 | fun jokeSelected(): LiveData = jokeSelected
42 |
43 | override fun multipleRandomJokes() {
44 | multipleRandomJokeUseCase.execute(JOKES_REQUESTED)
45 | .compose(schedulerProvider.doOnIoObserveOnMainSingle())
46 | .doOnSubscribe { loading.postValue(true) }
47 | .doFinally { loading.postValue(false) }
48 | .subscribe({ randomJokeDomainModels ->
49 | val randomJokeUiModels = getRandomJokeUiModels(randomJokeDomainModels)
50 | multipleRandomJokesRetrieved.postValue(randomJokeUiModels)
51 | }, { throwable ->
52 | Timber.d(throwable)
53 | errorResource.value = R.string.custom_joke_retrieving_error_generic
54 | }).also { subscriptions.add(it) }
55 | }
56 |
57 | override fun onJokeSelected(joke: String) {
58 | jokeSelected.postValue(joke)
59 | }
60 |
61 | private fun getRandomJokeUiModels(randomJokeDomainModels: List) =
62 | randomJokeDomainModels.map { randomJokeDomainModel ->
63 | randomJokeDomainToUiModelMapper.toUi(randomJokeDomainModel)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/presentation/src/main/java/prieto/fernando/presentation/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.main
2 |
3 | import android.app.Application
4 | import javax.inject.Inject
5 | import prieto.fernando.presentation.BaseViewModel
6 | import prieto.fernando.presentation.BaseViewModelInputs
7 | import prieto.fernando.presentation.BaseViewModelOutputs
8 |
9 | interface MainViewModelInputs : BaseViewModelInputs
10 |
11 | interface MainViewModelOutputs : BaseViewModelOutputs
12 |
13 | open class MainViewModel @Inject constructor() : BaseViewModel(),
14 | MainViewModelInputs,
15 | MainViewModelOutputs {
16 |
17 | override val inputs: MainViewModelInputs
18 | get() = this
19 |
20 | override val outputs: MainViewModelOutputs
21 | get() = this
22 | }
23 |
--------------------------------------------------------------------------------
/presentation/src/main/java/prieto/fernando/presentation/mapper/RandomJokeDomainToUiModelMapper.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.mapper
2 |
3 | import dagger.Reusable
4 | import prieto.fernando.data.CategoryDomainModel
5 | import prieto.fernando.data.RandomJokeDomainModel
6 | import prieto.fernando.presentation.CategoryUiModel
7 | import prieto.fernando.presentation.RandomJokeUiModel
8 | import javax.inject.Inject
9 |
10 | @Reusable
11 | class RandomJokeDomainToUiModelMapper @Inject constructor() {
12 | fun toUi(randomJokeDomainModel: RandomJokeDomainModel) =
13 | RandomJokeUiModel(
14 | id = randomJokeDomainModel.id,
15 | joke = randomJokeDomainModel.joke,
16 | categories = getCategories(randomJokeDomainModel.categories)
17 | )
18 |
19 | private fun getCategories(categories: List) =
20 | categories.map { categoryDomainModel ->
21 | when (categoryDomainModel) {
22 | CategoryDomainModel.EXPLICIT -> CategoryUiModel.EXPLICIT
23 | CategoryDomainModel.NERDY -> CategoryUiModel.NERDY
24 | else -> CategoryUiModel.UNKNOWN
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/presentation/src/main/java/prieto/fernando/presentation/model/NamesData.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.custom.model
2 |
3 | data class NamesData(
4 | val firstName: String,
5 | val lastName: String
6 | )
7 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | presentation
3 |
4 |
--------------------------------------------------------------------------------
/presentation/src/test/java/prieto/fernando/presentation/CustomJokeViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation
2 |
3 | import android.app.Application
4 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
5 | import androidx.lifecycle.Observer
6 | import com.nhaarman.mockito_kotlin.given
7 | import com.nhaarman.mockito_kotlin.mock
8 | import com.nhaarman.mockito_kotlin.times
9 | import com.nhaarman.mockito_kotlin.verify
10 | import io.reactivex.Single
11 | import junit.framework.Assert.assertEquals
12 | import org.junit.Before
13 | import org.junit.Rule
14 | import org.junit.Test
15 | import org.junit.rules.TestRule
16 | import org.junit.runner.RunWith
17 | import org.mockito.ArgumentCaptor
18 | import org.mockito.Mock
19 | import org.mockito.junit.MockitoJUnitRunner
20 | import prieto.fernando.presentation.custom.CustomJokeViewModel
21 | import prieto.fernando.presentation.custom.NamesButtonStateEvaluator
22 | import prieto.fernando.presentation.custom.model.NamesData
23 | import prieto.fernando.usecase.GetCustomRandomJokeUseCase
24 |
25 | @RunWith(MockitoJUnitRunner::class)
26 | class CustomJokeViewModelTest {
27 | private lateinit var cut: CustomJokeViewModel
28 |
29 | @Mock
30 | lateinit var application: Application
31 |
32 | @Mock
33 | lateinit var customJokeUseCase: GetCustomRandomJokeUseCase
34 |
35 | @Mock
36 | lateinit var buttonStateEvaluator: NamesButtonStateEvaluator
37 |
38 | @get:Rule
39 | var rule: TestRule = InstantTaskExecutorRule()
40 |
41 | private lateinit var customRandomJokeRetrievedTestObserver: Observer
42 | private lateinit var doneButtonEnabledTestObserver: Observer
43 |
44 | @Before
45 | fun setUp() {
46 | cut = CustomJokeViewModel(
47 | customJokeUseCase,
48 | buttonStateEvaluator
49 | )
50 | setupViewModelForTests(cut)
51 |
52 | customRandomJokeRetrievedTestObserver = mock()
53 | doneButtonEnabledTestObserver = mock()
54 | cut.customRandomJokeRetrieved().observeForever(customRandomJokeRetrievedTestObserver)
55 | cut.doneButtonEnabled().observeForever(doneButtonEnabledTestObserver)
56 | }
57 |
58 |
59 | @Test
60 | fun `Given names when customRandomJoke then customRandomJokeRetrieved invoked with expected value`() {
61 | // Given
62 | val firstName = "firstName"
63 | val lastName = "lastName"
64 |
65 | given(customJokeUseCase.execute(firstName, lastName)).willReturn(
66 | Single.just(mock())
67 | )
68 | val expected = Unit
69 |
70 | // When
71 | cut.customRandomJoke(firstName, lastName)
72 |
73 | // Then
74 | val captor = ArgumentCaptor.forClass(Unit::class.java)
75 | captor.run {
76 | verify(customRandomJokeRetrievedTestObserver, times(1)).onChanged(capture())
77 | assertEquals(expected, value)
78 | }
79 | }
80 |
81 | @Test
82 | fun `Given NamesData when onNamesChanged then doneButtonEnabled invoked`() {
83 | // Given
84 | val namesData =
85 | NamesData("firstName", "lastName")
86 | val expected = false
87 |
88 | given(
89 | buttonStateEvaluator.shouldEnableButton("firstName", "lastName")
90 | ).willReturn(expected)
91 |
92 | // When
93 | cut.onNamesChanged(namesData)
94 |
95 | // Then
96 | val captor = ArgumentCaptor.forClass(Boolean::class.java)
97 | captor.run {
98 | verify(doneButtonEnabledTestObserver, times(1)).onChanged(capture())
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/presentation/src/test/java/prieto/fernando/presentation/NamesButtonStateEvaluatorTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation
2 |
3 | import junit.framework.Assert.assertEquals
4 | import org.junit.Before
5 | import org.junit.Test
6 | import org.junit.runner.RunWith
7 | import org.junit.runners.Parameterized
8 | import prieto.fernando.presentation.custom.NamesButtonStateEvaluator
9 |
10 | @RunWith(Parameterized::class)
11 | class NamesButtonStateEvaluatorTest(
12 | private val givenNames: Names,
13 | private val expected: Boolean
14 | ) {
15 | companion object {
16 | @JvmStatic
17 | @Parameterized.Parameters
18 | fun data(): Collection> {
19 | return listOf(
20 | arrayOf(Names("", ""), false),
21 | arrayOf(Names(" ", " "), false),
22 | arrayOf(Names("SomeName", ""), false),
23 | arrayOf(Names("", "SomeLastName"), false),
24 | arrayOf(Names("Sh", ""), false),
25 | arrayOf(Names("", "Nn"), false),
26 | arrayOf(Names("Sh", "Nn"), false),
27 | arrayOf(
28 | Names(
29 | "NameReallyReallyReallySuperLong",
30 | "SomeLastName"
31 | ), false),
32 | arrayOf(
33 | Names(
34 | "",
35 | "LastNameReallyReallyLongLongg"
36 | ), false),
37 | arrayOf(Names("SomeName", "SomeLastName"), true),
38 | arrayOf(Names("Six", "The"), true),
39 | arrayOf(
40 | Names(
41 | "NameReallyReallySuperLong",
42 | "LastNameReallyReallyLongLongg"
43 | ), false),
44 | arrayOf(
45 | Names(
46 | "NameReallyReallySuperLong",
47 | "LastNameReallyReallyLong"
48 | ), true)
49 |
50 | )
51 | }
52 | }
53 |
54 | private lateinit var cut: NamesButtonStateEvaluator
55 |
56 | @Before
57 | fun setUp() {
58 | cut = NamesButtonStateEvaluator()
59 | }
60 |
61 | @Test
62 | fun `Given names when shouldEnableButton then return expected result`() {
63 | // When
64 | val actualValue = cut.shouldEnableButton(givenNames.firstName, givenNames.lastName)
65 |
66 | // Then
67 | assertEquals(expected, actualValue)
68 | }
69 | }
70 |
71 | data class Names(
72 | val firstName: String,
73 | val lastName: String
74 | )
75 |
--------------------------------------------------------------------------------
/presentation/src/test/java/prieto/fernando/presentation/ViewModelSetup.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation
2 |
3 | import prieto.fernando.presentation.scheduler.TestSchedulerProvider
4 |
5 | fun setupViewModelForTests(baseViewModel: BaseViewModel) {
6 | baseViewModel.schedulerProvider = TestSchedulerProvider()
7 | }
8 |
--------------------------------------------------------------------------------
/presentation/src/test/java/prieto/fernando/presentation/dashboard/DashboardViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.dashboard
2 |
3 | import android.app.Application
4 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
5 | import androidx.lifecycle.Observer
6 | import com.nhaarman.mockito_kotlin.mock
7 | import com.nhaarman.mockito_kotlin.times
8 | import com.nhaarman.mockito_kotlin.verify
9 | import com.nhaarman.mockito_kotlin.whenever
10 | import io.reactivex.Single
11 | import junit.framework.Assert.assertEquals
12 | import org.junit.Before
13 | import org.junit.Rule
14 | import org.junit.Test
15 | import org.junit.rules.TestRule
16 | import org.junit.runner.RunWith
17 | import org.mockito.ArgumentCaptor
18 | import org.mockito.Mock
19 | import org.mockito.junit.MockitoJUnitRunner
20 | import prieto.fernando.data.RandomJokeDomainModel
21 | import prieto.fernando.presentation.R
22 | import prieto.fernando.presentation.data.RandomJokeAndTitleResource
23 | import prieto.fernando.presentation.RandomJokeUiModel
24 | import prieto.fernando.presentation.mapper.RandomJokeDomainToUiModelMapper
25 | import prieto.fernando.presentation.setupViewModelForTests
26 | import prieto.fernando.usecase.GetCustomRandomJokeUseCase
27 | import prieto.fernando.usecase.GetRandomJokeUseCase
28 | import prieto.fernando.usecase.ResetCustomRandomJokeUseCase
29 |
30 | @RunWith(MockitoJUnitRunner::class)
31 | class DashboardViewModelTest {
32 | private lateinit var cut: DashboardViewModel
33 |
34 | @Mock
35 | lateinit var application: Application
36 |
37 | @Mock
38 | lateinit var customRandomJokeUseCase: GetCustomRandomJokeUseCase
39 |
40 | @Mock
41 | lateinit var randomJokeUseCase: GetRandomJokeUseCase
42 |
43 | @Mock
44 | lateinit var resetCustomRandomJokeUseCase: ResetCustomRandomJokeUseCase
45 |
46 | @Mock
47 | lateinit var randomJokeDomainToUiModelMapper: RandomJokeDomainToUiModelMapper
48 |
49 | @get:Rule
50 | var rule: TestRule = InstantTaskExecutorRule()
51 |
52 | private lateinit var navigateToCustomJokeTestObserver: Observer
53 | private lateinit var navigateToInfiniteJokesTestObserver: Observer
54 | private lateinit var randomJokeRetrievedTestObserver: Observer
55 | private lateinit var customRandomJokeRetrievedTestObserver: Observer
56 | private lateinit var errorResourceTestObserver: Observer
57 |
58 | @Before
59 | fun setUp() {
60 | cut = DashboardViewModel(
61 | customRandomJokeUseCase,
62 | randomJokeUseCase,
63 | resetCustomRandomJokeUseCase,
64 | randomJokeDomainToUiModelMapper
65 | )
66 | setupViewModelForTests(cut)
67 |
68 | navigateToCustomJokeTestObserver = mock()
69 | navigateToInfiniteJokesTestObserver = mock()
70 | randomJokeRetrievedTestObserver = mock()
71 | customRandomJokeRetrievedTestObserver = mock()
72 | errorResourceTestObserver = mock()
73 | cut.navigateToCustomJoke().observeForever(navigateToCustomJokeTestObserver)
74 | cut.navigateToInfiniteJokes().observeForever(navigateToInfiniteJokesTestObserver)
75 | cut.randomJokeRetrieved().observeForever(randomJokeRetrievedTestObserver)
76 | cut.customRandomJokeRetrieved().observeForever(customRandomJokeRetrievedTestObserver)
77 | cut.errorResource().observeForever(errorResourceTestObserver)
78 | }
79 |
80 | @Test
81 | fun `When randomJoke then randomJokeRetrieved invoked with expected result`() {
82 | // Given
83 | val randomJokeDomainModel = RandomJokeDomainModel(
84 | "some Id",
85 | "iOS is a good OS",
86 | emptyList()
87 | )
88 | val randomJokeUiModel = RandomJokeUiModel(
89 | "some Id",
90 | "iOS is a good OS",
91 | emptyList()
92 | )
93 | whenever(randomJokeDomainToUiModelMapper.toUi(randomJokeDomainModel)).thenReturn(
94 | randomJokeUiModel
95 | )
96 | val stringResource = R.string.dashboard_dialog_title
97 | val expected = RandomJokeAndTitleResource(
98 | randomJokeUiModel,
99 | stringResource
100 | )
101 | whenever(randomJokeUseCase.execute()).thenReturn(
102 | Single.just(randomJokeDomainModel)
103 | )
104 | // When
105 | cut.randomJoke()
106 |
107 | // Then
108 | val captor = ArgumentCaptor.forClass(RandomJokeAndTitleResource::class.java)
109 | captor.run {
110 | verify(randomJokeRetrievedTestObserver, times(1)).onChanged(capture())
111 | assertEquals(expected, value)
112 | }
113 | }
114 |
115 | @Test
116 | fun `When onCustomRandomJokeClicked then navigateToCustomJoke invoked`() {
117 | // When
118 | cut.onCustomRandomJokeClicked()
119 |
120 | // Then
121 | val captor = ArgumentCaptor.forClass(Unit::class.java)
122 | captor.run {
123 | verify(navigateToCustomJokeTestObserver, times(1)).onChanged(capture())
124 | }
125 | }
126 |
127 | @Test
128 | fun `When onMultipleJokesClicked then customRandomJokeRetrieved invoked`() {
129 | // When
130 | cut.onMultipleJokesClicked()
131 |
132 | // Then
133 | val captor = ArgumentCaptor.forClass(Unit::class.java)
134 | captor.run {
135 | verify(navigateToInfiniteJokesTestObserver, times(1)).onChanged(capture())
136 | }
137 | }
138 |
139 | @Test
140 | fun `When customRandomJokeForDialog then customRandomJokeRetrieved invoked with expected result`() {
141 | // Given
142 | val randomJokeDomainModel = RandomJokeDomainModel(
143 | "some Id",
144 | "iOS is a good OS",
145 | emptyList()
146 | )
147 | val randomJokeUiModel = RandomJokeUiModel(
148 | "some Id",
149 | "iOS is a good OS",
150 | emptyList()
151 | )
152 | whenever(randomJokeDomainToUiModelMapper.toUi(randomJokeDomainModel)).thenReturn(
153 | randomJokeUiModel
154 | )
155 | val stringResource = R.string.custom_joke_dialog_title
156 | val expected = RandomJokeAndTitleResource(
157 | randomJokeUiModel,
158 | stringResource
159 | )
160 | whenever(customRandomJokeUseCase.execute(null, null)).thenReturn(
161 | Single.just(randomJokeDomainModel)
162 | )
163 |
164 | // When
165 | cut.customRandomJokeForDialog()
166 |
167 | // Then
168 | val captor = ArgumentCaptor.forClass(RandomJokeAndTitleResource::class.java)
169 | captor.run {
170 | verify(customRandomJokeRetrievedTestObserver, times(1)).onChanged(capture())
171 | assertEquals(expected, value)
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/presentation/src/test/java/prieto/fernando/presentation/infinite/InfiniteJokesViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.infinite
2 |
3 | import android.app.Application
4 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
5 | import androidx.lifecycle.Observer
6 | import com.nhaarman.mockito_kotlin.mock
7 | import com.nhaarman.mockito_kotlin.times
8 | import com.nhaarman.mockito_kotlin.verify
9 | import com.nhaarman.mockito_kotlin.whenever
10 | import io.reactivex.Single
11 | import junit.framework.Assert.assertEquals
12 | import org.junit.Before
13 | import org.junit.Rule
14 | import org.junit.Test
15 | import org.junit.rules.TestRule
16 | import org.junit.runner.RunWith
17 | import org.mockito.ArgumentCaptor
18 | import org.mockito.Mock
19 | import org.mockito.junit.MockitoJUnitRunner
20 | import prieto.fernando.data.RandomJokeDomainModel
21 | import prieto.fernando.presentation.RandomJokeUiModel
22 | import prieto.fernando.presentation.mapper.RandomJokeDomainToUiModelMapper
23 | import prieto.fernando.presentation.setupViewModelForTests
24 | import prieto.fernando.usecase.GetMultipleRandomJokeUseCase
25 |
26 | @RunWith(MockitoJUnitRunner::class)
27 | class InfiniteJokesViewModelTest {
28 | private lateinit var cut: InfiniteJokesViewModel
29 |
30 | @Mock
31 | lateinit var application: Application
32 |
33 | @Mock
34 | lateinit var multipleRandomJokeUseCase: GetMultipleRandomJokeUseCase
35 |
36 | @Mock
37 | lateinit var randomJokeDomainToUiModelMapper: RandomJokeDomainToUiModelMapper
38 |
39 | @get:Rule
40 | var rule: TestRule = InstantTaskExecutorRule()
41 |
42 | private lateinit var multipleRandomJokesRetrievedTestObserver: Observer>
43 |
44 | @Before
45 | fun setUp() {
46 | cut = InfiniteJokesViewModel(
47 | multipleRandomJokeUseCase,
48 | randomJokeDomainToUiModelMapper
49 | )
50 | setupViewModelForTests(cut)
51 |
52 | multipleRandomJokesRetrievedTestObserver = mock()
53 | cut.multipleRandomJokesRetrieved().observeForever(multipleRandomJokesRetrievedTestObserver)
54 | }
55 |
56 | @Test
57 | fun `When multipleRandomJokes then multipleRandomJokesRetrieved invoked`() {
58 | // Given
59 | val randomJokeDomainModel = RandomJokeDomainModel(
60 | "some Id",
61 | "iOS is a good OS",
62 | emptyList()
63 | )
64 | val randomJokeUiModel = RandomJokeUiModel(
65 | "some Id",
66 | "iOS is a good OS",
67 | emptyList()
68 | )
69 | val randomJokeUiModelList = listOf(randomJokeUiModel)
70 | whenever(randomJokeDomainToUiModelMapper.toUi(randomJokeDomainModel)).thenReturn(
71 | randomJokeUiModel
72 | )
73 | whenever(multipleRandomJokeUseCase.execute(12)).thenReturn(
74 | Single.just(listOf(randomJokeDomainModel))
75 | )
76 | // When
77 | cut.multipleRandomJokes()
78 |
79 | // Then
80 | val captor = ArgumentCaptor.forClass(RandomJokeUiModel::class.java)
81 | captor.run {
82 | verify(multipleRandomJokesRetrievedTestObserver, times(1)).onChanged(listOf(capture()))
83 | assertEquals(randomJokeUiModelList, value)
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/presentation/src/test/java/prieto/fernando/presentation/mapper/RandomJokeDomainToUiModelMapperTest.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.mapper
2 |
3 | import junit.framework.Assert.assertEquals
4 | import org.junit.Before
5 | import org.junit.Test
6 | import prieto.fernando.data.CategoryDomainModel
7 | import prieto.fernando.data.RandomJokeDomainModel
8 | import prieto.fernando.presentation.CategoryUiModel
9 | import prieto.fernando.presentation.RandomJokeUiModel
10 |
11 | class RandomJokeDomainToUiModelMapperTest {
12 | private lateinit var cut: RandomJokeDomainToUiModelMapper
13 |
14 | @Before
15 | fun setUp() {
16 | cut = RandomJokeDomainToUiModelMapper()
17 | }
18 |
19 | @Test
20 | fun `Given RandomJokeDomainModel when toUi then return RandomJokeUiModel`() {
21 | // Given
22 | val randomJokeDomainModel = RandomJokeDomainModel(
23 | "some Id",
24 | "iOS is a good OS",
25 | listOf(CategoryDomainModel.NERDY, CategoryDomainModel.EXPLICIT)
26 | )
27 |
28 | val expectedResult = RandomJokeUiModel(
29 | "some Id",
30 | "iOS is a good OS",
31 | listOf(
32 | CategoryUiModel.NERDY,
33 | CategoryUiModel.EXPLICIT
34 | )
35 | )
36 |
37 | // When
38 | val actualValue = cut.toUi(randomJokeDomainModel)
39 |
40 | // Then
41 | assertEquals(expectedResult, actualValue)
42 | }
43 |
44 | @Test
45 | fun `Given RandomJokeDomainModel with empty categories when toUi then return RandomJokeUiModel`() {
46 | // Given
47 | val randomJokeDomainModel = RandomJokeDomainModel(
48 | "some Id",
49 | "iOS is a good OS",
50 | emptyList()
51 | )
52 |
53 | val expectedResult = RandomJokeUiModel(
54 | "some Id",
55 | "iOS is a good OS",
56 | emptyList()
57 | )
58 |
59 | // When
60 | val actualValue = cut.toUi(randomJokeDomainModel)
61 |
62 | // Then
63 | assertEquals(expectedResult, actualValue)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/presentation/src/test/java/prieto/fernando/presentation/scheduler/TestSchedulerProvider.kt:
--------------------------------------------------------------------------------
1 | package prieto.fernando.presentation.scheduler
2 |
3 | import io.reactivex.Scheduler
4 | import io.reactivex.schedulers.Schedulers
5 | import prieto.fernando.presentation.BaseSchedulerProvider
6 |
7 | class TestSchedulerProvider : prieto.fernando.presentation.BaseSchedulerProvider() {
8 | override fun io(): Scheduler = Schedulers.trampoline()
9 | override fun ui(): Scheduler = Schedulers.trampoline()
10 | override fun computation(): Scheduler = Schedulers.trampoline()
11 | override fun newThread(): Scheduler = Schedulers.trampoline()
12 | }
13 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':core', ':data-jokesapi', ':domain', ':data-cache', ':presentation',':navigation'
2 | rootProject.name='JokesApp'
--------------------------------------------------------------------------------