├── .gitignore
├── LICENSE
├── README.md
├── build.gradle
├── docs
└── images
│ ├── bwi.png
│ ├── bwp.png
│ ├── bwu.png
│ ├── rxpm_diagram.png
│ └── rxpm_vs_mvp_vs_mvvm.png
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── rxpm
├── .gitignore
├── build.gradle
├── publish-mavencentral.gradle
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ └── kotlin
│ │ └── me
│ │ └── dmdev
│ │ └── rxpm
│ │ ├── Action.kt
│ │ ├── Command.kt
│ │ ├── PmExtensions.kt
│ │ ├── PmView.kt
│ │ ├── PresentationModel.kt
│ │ ├── State.kt
│ │ ├── base
│ │ ├── PmActivity.kt
│ │ ├── PmBottomSheetDialogFragment.kt
│ │ ├── PmController.kt
│ │ ├── PmDialogFragment.kt
│ │ └── PmFragment.kt
│ │ ├── delegate
│ │ ├── CommonDelegate.kt
│ │ ├── PmActivityDelegate.kt
│ │ ├── PmControllerDelegate.kt
│ │ ├── PmFragmentDelegate.kt
│ │ └── PmStore.kt
│ │ ├── navigation
│ │ ├── ActivityNavigationMessageDispatcher.kt
│ │ ├── ControllerNavigationMessageDispatcher.kt
│ │ ├── FragmentNavigationMessageDispatcher.kt
│ │ ├── NavigationMessage.kt
│ │ ├── NavigationMessageDispatcher.kt
│ │ ├── NavigationMessageHandler.kt
│ │ ├── NavigationalPm.kt
│ │ └── NotHandledNavigationMessageException.kt
│ │ ├── test
│ │ └── PmTestHelper.kt
│ │ ├── util
│ │ ├── BufferSingleValueWhileIdleOperator.kt
│ │ └── BufferWhileIdleOperator.kt
│ │ ├── validation
│ │ ├── CheckValidator.kt
│ │ ├── FormValidator.kt
│ │ ├── InputValidator.kt
│ │ └── Validator.kt
│ │ └── widget
│ │ ├── CheckControl.kt
│ │ ├── DialogControl.kt
│ │ └── InputControl.kt
│ └── test
│ ├── kotlin
│ └── me
│ │ └── dmdev
│ │ └── rxpm
│ │ ├── ChildPresentationModelTest.kt
│ │ ├── PmExtensionsTest.kt
│ │ ├── PresentationModelTest.kt
│ │ ├── StateTest.kt
│ │ ├── delegate
│ │ ├── CommonDelegateTest.kt
│ │ ├── PmActivityDelegateTest.kt
│ │ ├── PmControllerDelegateTest.kt
│ │ └── PmFragmentDelegateTest.kt
│ │ ├── navigation
│ │ └── NavigationMessageDispatcherTest.kt
│ │ ├── test
│ │ └── PmTestHelperTest.kt
│ │ ├── util
│ │ └── SchedulersRule.kt
│ │ ├── validation
│ │ ├── CheckValidatorTest.kt
│ │ ├── FormValidatorTest.kt
│ │ └── InputValidatorTest.kt
│ │ └── widget
│ │ ├── CheckControlTest.kt
│ │ ├── DialogControlTest.kt
│ │ └── InputControlTest.kt
│ └── resources
│ └── mockito-extensions
│ └── org.mockito.plugins.MockMaker
├── sample
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── kotlin
│ └── me
│ │ └── dmdev
│ │ └── rxpm
│ │ └── sample
│ │ ├── App.kt
│ │ ├── LaunchActivity.kt
│ │ ├── counter
│ │ ├── CounterActivity.kt
│ │ └── CounterPm.kt
│ │ ├── main
│ │ ├── MainActivity.kt
│ │ ├── MainComponent.kt
│ │ ├── Messages.kt
│ │ ├── api
│ │ │ ├── ServerApi.kt
│ │ │ └── ServerApiSimulator.kt
│ │ ├── extensions
│ │ │ ├── FragmentManagerExtensions.kt
│ │ │ └── UiExtensions.kt
│ │ ├── model
│ │ │ ├── AuthModel.kt
│ │ │ └── TokenStorage.kt
│ │ ├── ui
│ │ │ ├── base
│ │ │ │ ├── BackHandler.kt
│ │ │ │ ├── ProgressDialog.kt
│ │ │ │ ├── Screen.kt
│ │ │ │ └── ScreenPresentationModel.kt
│ │ │ ├── confirmation
│ │ │ │ ├── CodeConfirmationPm.kt
│ │ │ │ └── CodeConfirmationScreen.kt
│ │ │ ├── country
│ │ │ │ ├── ChooseCountryPm.kt
│ │ │ │ ├── ChooseCountryScreen.kt
│ │ │ │ └── CountriesAdapter.kt
│ │ │ ├── main
│ │ │ │ ├── MainPm.kt
│ │ │ │ └── MainScreen.kt
│ │ │ └── phone
│ │ │ │ ├── AuthByPhonePm.kt
│ │ │ │ └── AuthByPhoneScreen.kt
│ │ └── util
│ │ │ ├── Country.kt
│ │ │ ├── PhoneUtil.kt
│ │ │ └── ResourcesProvider.kt
│ │ └── validation
│ │ ├── FormValidationActivity.kt
│ │ └── FormValidationPm.kt
│ └── res
│ ├── drawable
│ ├── bg_edit_country.xml
│ ├── ic_add_white_24dp.xml
│ ├── ic_arrow_back_white_24dp.xml
│ ├── ic_close_white_24dp.xml
│ ├── ic_exit_to_app_white_24dp.xml
│ ├── ic_remove_white_24dp.xml
│ └── ic_search_white_24dp.xml
│ ├── layout
│ ├── activity_counter.xml
│ ├── activity_form.xml
│ ├── activity_launch.xml
│ ├── activity_main.xml
│ ├── item_country.xml
│ ├── layout_loading_view.xml
│ ├── screen_auth_by_phone.xml
│ ├── screen_choose_country.xml
│ ├── screen_code_confirmation.xml
│ └── screen_main.xml
│ ├── menu
│ └── main.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.png
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Build files and folders
2 | .gradle/
3 | build/
4 |
5 | # Properties
6 | local.properties
7 |
8 | # Idea
9 | .idea/
10 | **/*.iml
11 |
12 | # Mac OS
13 | .DS_Store
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
4 | and Vasili Chyrvon (vasili.chyrvon@gmail.com)
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlinVersion = '1.4.30'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | jcenter()
7 | }
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:4.1.2'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | mavenCentral()
18 | jcenter()
19 | }
20 | }
21 |
22 | task clean(type: Delete) {
23 | delete rootProject.buildDir
24 | }
25 |
26 | ext {
27 |
28 | minSdkVersion = 16
29 | compileSdkVersion = 30
30 | targetSdkVersion = 30
31 | rxBindingVersion = '3.1.0'
32 |
33 | annotation = 'androidx.annotation:annotation:1.2.0-beta01'
34 | appCompat = 'androidx.appcompat:appcompat:1.3.0-beta01'
35 | materialDesign = 'com.google.android.material:material:1.3.0'
36 |
37 | kotlinStdlib = "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
38 |
39 | rxJava2 = "io.reactivex.rxjava2:rxjava:2.2.20"
40 | rxRelay2 = "com.jakewharton.rxrelay2:rxrelay:2.1.1"
41 | rxAndroid2 = "io.reactivex.rxjava2:rxandroid:2.1.1"
42 |
43 | rxBinding = "com.jakewharton.rxbinding3:rxbinding:$rxBindingVersion"
44 | rxBindingAppCompat = "com.jakewharton.rxbinding3:rxbinding-appcompat:$rxBindingVersion"
45 |
46 | conductor = "com.bluelinelabs:conductor:3.0.1"
47 |
48 | junitKotlin = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion"
49 | mockitoKotlin = 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
50 | }
--------------------------------------------------------------------------------
/docs/images/bwi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/docs/images/bwi.png
--------------------------------------------------------------------------------
/docs/images/bwp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/docs/images/bwp.png
--------------------------------------------------------------------------------
/docs/images/bwu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/docs/images/bwu.png
--------------------------------------------------------------------------------
/docs/images/rxpm_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/docs/images/rxpm_diagram.png
--------------------------------------------------------------------------------
/docs/images/rxpm_vs_mvp_vs_mvvm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/docs/images/rxpm_vs_mvp_vs_mvvm.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | org.gradle.parallel=true
18 |
19 | android.enableJetifier=true
20 | android.useAndroidX=true
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Feb 07 20:50:34 MSK 2021
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-6.5-all.zip
7 |
--------------------------------------------------------------------------------
/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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
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 Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/rxpm/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/rxpm/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion rootProject.compileSdkVersion
6 |
7 | defaultConfig {
8 | minSdkVersion rootProject.minSdkVersion
9 | targetSdkVersion rootProject.targetSdkVersion
10 | }
11 |
12 | buildTypes {
13 | release {
14 | minifyEnabled false
15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'consumer-proguard-rules.txt'
16 | }
17 | }
18 |
19 | compileOptions {
20 | sourceCompatibility JavaVersion.VERSION_1_8
21 | targetCompatibility JavaVersion.VERSION_1_8
22 | }
23 |
24 | sourceSets {
25 | main.java.srcDirs += 'src/main/kotlin'
26 | test.java.srcDirs += 'src/test/kotlin'
27 | }
28 | }
29 |
30 | dependencies {
31 |
32 | // For default implementations
33 | compileOnly rootProject.appCompat
34 | compileOnly rootProject.materialDesign
35 | compileOnly rootProject.conductor
36 |
37 | // Rx
38 | api rootProject.rxJava2
39 | implementation rootProject.annotation
40 | implementation rootProject.rxRelay2
41 | implementation rootProject.rxAndroid2
42 | implementation rootProject.rxBinding
43 |
44 | // For tests
45 | testImplementation rootProject.junitKotlin
46 | testImplementation rootProject.mockitoKotlin
47 | testImplementation rootProject.conductor
48 | testImplementation rootProject.appCompat
49 | }
50 |
51 | ext {
52 | PUBLISH_GROUP_ID = 'me.dmdev.rxpm'
53 | PUBLISH_VERSION = '2.1.2'
54 | PUBLISH_ARTIFACT_ID = 'rxpm'
55 | }
56 |
57 | apply from: "publish-mavencentral.gradle"
--------------------------------------------------------------------------------
/rxpm/publish-mavencentral.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven-publish'
2 | apply plugin: 'signing'
3 |
4 | task androidSourcesJar(type: Jar) {
5 | archiveClassifier.set('sources')
6 | if (project.plugins.findPlugin("com.android.library")) {
7 | from android.sourceSets.main.java.srcDirs
8 | } else {
9 | from sourceSets.main.java.srcDirs
10 | }
11 | }
12 |
13 | artifacts {
14 | archives androidSourcesJar
15 | }
16 |
17 |
18 | group = PUBLISH_GROUP_ID
19 | version = PUBLISH_VERSION
20 |
21 | ext["signing.keyId"] = ''
22 | ext["signing.password"] = ''
23 | ext["signing.secretKeyRingFile"] = ''
24 | ext["ossrhUsername"] = ''
25 | ext["ossrhPassword"] = ''
26 | ext["sonatypeStagingProfileId"] = ''
27 |
28 | File secretPropsFile = project.rootProject.file('local.properties')
29 | if (secretPropsFile.exists()) {
30 | Properties p = new Properties()
31 | p.load(new FileInputStream(secretPropsFile))
32 | p.each { name, value ->
33 | ext[name] = value
34 | }
35 | } else {
36 | ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
37 | ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
38 | ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')
39 | ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
40 | ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
41 | ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
42 | }
43 |
44 | publishing {
45 | publications {
46 | release(MavenPublication) {
47 | groupId PUBLISH_GROUP_ID
48 | artifactId PUBLISH_ARTIFACT_ID
49 | version PUBLISH_VERSION
50 | if (project.plugins.findPlugin("com.android.library")) {
51 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
52 | } else {
53 | artifact("$buildDir/libs/${project.getName()}-${version}.jar")
54 | }
55 |
56 | artifact androidSourcesJar
57 |
58 | pom {
59 | name = PUBLISH_ARTIFACT_ID
60 | description = 'Reactive implementation of Presentation Model pattern in Android'
61 | url = 'https://github.com/dmdevgo/RxPM'
62 | licenses {
63 | license {
64 | name = 'MIT'
65 | url = 'https://github.com/dmdevgo/RxPM/blob/develop/LICENSE'
66 | }
67 | }
68 | developers {
69 | developer {
70 | id = 'dmdev'
71 | name = 'Dmitriy Gorbunov'
72 | email = 'dmitriy.goto@gmail.com'
73 | }
74 | developer {
75 | id = 'jeevuz'
76 | name = 'Vasili Chyrvon'
77 | email = 'vasili.chyrvon@gmail.com'
78 | }
79 | }
80 | scm {
81 | connection = 'scm:git@github.com:dmdevgo/RxPM.git'
82 | developerConnection = 'scm:git@github.com:dmdevgo/RxPM.git'
83 | url = 'https://github.com/dmdevgo/RxPM'
84 | }
85 | withXml {
86 | def dependenciesNode = asNode().appendNode('dependencies')
87 |
88 | project.configurations.implementation.allDependencies.each {
89 | def dependencyNode = dependenciesNode.appendNode('dependency')
90 | dependencyNode.appendNode('groupId', it.group)
91 | dependencyNode.appendNode('artifactId', it.name)
92 | dependencyNode.appendNode('version', it.version)
93 | }
94 | }
95 | }
96 | }
97 | }
98 | repositories {
99 | maven {
100 | name = "sonatype"
101 |
102 | def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
103 | def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/"
104 | url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
105 |
106 | credentials {
107 | username ossrhUsername
108 | password ossrhPassword
109 | }
110 | }
111 | }
112 | }
113 |
114 | signing {
115 | sign publishing.publications
116 | }
--------------------------------------------------------------------------------
/rxpm/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/Action.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm
27 |
28 | import android.annotation.*
29 | import com.jakewharton.rxrelay2.*
30 | import io.reactivex.*
31 | import io.reactivex.android.schedulers.*
32 |
33 | /**
34 | * Reactive property for the actions from the [view][PmView].
35 | * Can be changed and observed in reactive manner with it's [consumer] and [PresentationModel.observable].
36 | *
37 | * Use to send actions of the view, e.g. some widget's clicks.
38 | *
39 | * @see State
40 | * @see Command
41 | */
42 | class Action internal constructor(internal val pm: PresentationModel) {
43 |
44 | internal val relay = PublishRelay.create().toSerialized()
45 |
46 | /**
47 | * Consumer of the [Action][Action].
48 | */
49 | val consumer get() = relay.asConsumer()
50 | }
51 |
52 | /**
53 | * Creates the [Action].
54 | * Optionally subscribes the [action chain][actionChain] to this action.
55 | * This chain will be unsubscribed ON [DESTROY][PresentationModel.Lifecycle.DESTROYED].
56 | */
57 | @SuppressLint("CheckResult")
58 | fun PresentationModel.action(
59 | actionChain: (Observable.() -> Observable<*>)? = null
60 | ): Action {
61 | val action = Action(pm = this)
62 |
63 | if (actionChain != null) {
64 | lifecycleObservable
65 | .filter { it == PresentationModel.Lifecycle.CREATED }
66 | .take(1)
67 | .subscribe {
68 | actionChain.let { chain ->
69 | action.relay
70 | .chain()
71 | .retry()
72 | .subscribe()
73 | .untilDestroy()
74 | }
75 | }
76 | }
77 |
78 | return action
79 | }
80 |
81 | /**
82 | * Subscribes [Action][Action] to the observable and adds it to the subscriptions list
83 | * that will be CLEARED ON [UNBIND][PresentationModel.Lifecycle.UNBINDED],
84 | * so use it ONLY in [PmView.onBindPresentationModel].
85 | */
86 | infix fun Observable.bindTo(action: Action) {
87 | with(action.pm) {
88 | this@bindTo.observeOn(AndroidSchedulers.mainThread())
89 | .subscribe(action.consumer)
90 | .untilUnbind()
91 | }
92 | }
93 |
94 | /**
95 | * Pass the value to the [Action][Action].
96 | */
97 | infix fun T.passTo(action: Action) {
98 | action.consumer.accept(this)
99 | }
100 |
101 | /**
102 | * Pass an empty value to the [Action][Action].
103 | */
104 | infix fun Unit.passTo(action: Action) {
105 | action.consumer.accept(Unit)
106 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/Command.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm
27 |
28 | import com.jakewharton.rxrelay2.*
29 | import io.reactivex.*
30 | import io.reactivex.functions.*
31 |
32 | /**
33 | * Reactive property for the commands to the [view][PmView].
34 | * Can be observed and changed in reactive manner with it's [observable] and [PresentationModel.consumer].
35 | *
36 | * Use to represent a command to the view, e.g. toast or dialog showing.
37 | *
38 | * @param isIdle observable, that shows when `command` need to buffer the values (while isIdle value is true).
39 | * Buffered values will be delivered later (when isIdle emits false).
40 | * By default (when null is passed) it will buffer while the [view][PmView] is paused.
41 | *
42 | * @param bufferSize how many values should be kept in buffer. Null means no restrictions.
43 | *
44 | * @see Action
45 | * @see Command
46 | */
47 | class Command internal constructor(
48 | internal val pm: PresentationModel,
49 | isIdle: Observable? = null,
50 | bufferSize: Int? = null
51 | ) {
52 | internal val relay = PublishRelay.create().toSerialized()
53 |
54 | /**
55 | * Observable of this [Command].
56 | */
57 | val observable: Observable =
58 | if (bufferSize == 0) {
59 | relay.asObservable()
60 | } else {
61 | if (isIdle == null) {
62 | relay.bufferWhileIdle(pm.paused, bufferSize)
63 | } else {
64 | relay.bufferWhileIdle(isIdle, bufferSize)
65 | }
66 | }
67 | .publish()
68 | .apply { connect() }
69 | }
70 |
71 | /**
72 | * Creates the [Command].
73 | *
74 | * @param isIdle observable, that shows when `command` need to buffer the values (while isIdle value is true).
75 | * Buffered values will be delivered later (when isIdle emits false).
76 | * By default (when null is passed) it will buffer while the [view][PmView] is unbind from the [PresentationModel].
77 | *
78 | * @param bufferSize how many values should be kept in buffer. Null means no restrictions.
79 | */
80 | fun PresentationModel.command(
81 | isIdle: Observable? = null,
82 | bufferSize: Int? = null): Command {
83 |
84 | return Command(this, isIdle, bufferSize)
85 | }
86 |
87 | /**
88 | * Subscribes to the [Command][Command] and adds it to the subscriptions list
89 | * that will be CLEARED ON [UNBIND][PresentationModel.Lifecycle.UNBINDED],
90 | * so use it ONLY in [PmView.onBindPresentationModel].
91 | */
92 | infix fun Command.bindTo(consumer: Consumer) {
93 | with(pm) {
94 | this@bindTo.observable
95 | .subscribe(consumer)
96 | .untilUnbind()
97 | }
98 | }
99 |
100 | /**
101 | * Subscribe to the [Command][Command] and adds it to the subscriptions list
102 | * that will be CLEARED ON [UNBIND][PresentationModel.Lifecycle.UNBINDED],
103 | * so use it ONLY in [PmView.onBindPresentationModel].
104 | */
105 | infix fun Command.bindTo(consumer: (T) -> Unit) {
106 | with(pm) {
107 | this@bindTo.observable
108 | .subscribe(consumer)
109 | .untilUnbind()
110 | }
111 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/PmView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm
27 |
28 | /**
29 | * Interface that need to be implemented by the View part of the RxPM pattern.
30 | * Has a few useful callbacks and extensions.
31 | */
32 | interface PmView {
33 |
34 | /**
35 | * [PresentationModel] for this view.
36 | */
37 | val presentationModel: PM
38 |
39 | /**
40 | * Provide presentation model to use with this fragment.
41 | */
42 | fun providePresentationModel(): PM
43 |
44 | /**
45 | * Bind to the [Presentation Model][presentationModel] in that method.
46 | */
47 | fun onBindPresentationModel(pm: PM)
48 |
49 | /**
50 | * Called when the view unbinds from the [Presentation Model][presentationModel].
51 | */
52 | fun onUnbindPresentationModel() {
53 | // Nо-op. Override if you need it.
54 | }
55 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/base/PmActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.base
27 |
28 | import android.os.Bundle
29 | import androidx.appcompat.app.AppCompatActivity
30 | import me.dmdev.rxpm.PmView
31 | import me.dmdev.rxpm.PresentationModel
32 | import me.dmdev.rxpm.delegate.PmActivityDelegate
33 | import me.dmdev.rxpm.delegate.PmActivityDelegate.RetainMode
34 |
35 | /**
36 | * Predefined [Activity][AppCompatActivity] implementing the [PmView][PmView].
37 | *
38 | * Just override the [providePresentationModel] and [onBindPresentationModel] methods and you are good to go.
39 | *
40 | * If extending is not possible you can implement [PmView],
41 | * create a [PmActivityDelegate] and pass the lifecycle callbacks to it.
42 | * See this class's source code for the example.
43 | */
44 | abstract class PmActivity : AppCompatActivity(), PmView {
45 |
46 | private val delegate by lazy(LazyThreadSafetyMode.NONE) {
47 | PmActivityDelegate(this, RetainMode.CONFIGURATION_CHANGES)
48 | }
49 |
50 | final override val presentationModel get() = delegate.presentationModel
51 |
52 | override fun onCreate(savedInstanceState: Bundle?) {
53 | super.onCreate(savedInstanceState)
54 | delegate.onCreate(savedInstanceState)
55 | }
56 |
57 | override fun onPostCreate(savedInstanceState: Bundle?) {
58 | super.onPostCreate(savedInstanceState)
59 | delegate.onPostCreate()
60 | }
61 |
62 | override fun onStart() {
63 | super.onStart()
64 | delegate.onStart()
65 | }
66 |
67 | override fun onResume() {
68 | super.onResume()
69 | delegate.onResume()
70 | }
71 |
72 | override fun onSaveInstanceState(outState: Bundle) {
73 | delegate.onSaveInstanceState(outState)
74 | super.onSaveInstanceState(outState)
75 | }
76 |
77 | override fun onPause() {
78 | delegate.onPause()
79 | super.onPause()
80 | }
81 |
82 | override fun onStop() {
83 | delegate.onStop()
84 | super.onStop()
85 | }
86 |
87 | override fun onDestroy() {
88 | delegate.onDestroy()
89 | super.onDestroy()
90 | }
91 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/base/PmBottomSheetDialogFragment.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.base
27 |
28 | import android.os.Bundle
29 | import android.view.View
30 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
31 | import me.dmdev.rxpm.PmView
32 | import me.dmdev.rxpm.PresentationModel
33 | import me.dmdev.rxpm.delegate.PmFragmentDelegate
34 | import me.dmdev.rxpm.delegate.PmFragmentDelegate.RetainMode
35 |
36 | /**
37 | * Predefined [BottomSheetDialogFragment] implementing the [PmView][PmView].
38 | *
39 | * Just override the [providePresentationModel] and [onBindPresentationModel] methods and you are good to go.
40 | *
41 | * If extending is not possible you can implement [PmView],
42 | * create a [PmFragmentDelegate] and pass the lifecycle callbacks to it.
43 | * See this class's source code for the example.
44 | */
45 | abstract class PmBottomSheetDialogFragment
46 | : BottomSheetDialogFragment(), PmView {
47 |
48 | private val delegate by lazy(LazyThreadSafetyMode.NONE) {
49 | PmFragmentDelegate(this, RetainMode.CONFIGURATION_CHANGES)
50 | }
51 |
52 | final override val presentationModel get() = delegate.presentationModel
53 |
54 | override fun onCreate(savedInstanceState: Bundle?) {
55 | super.onCreate(savedInstanceState)
56 | delegate.onCreate(savedInstanceState)
57 | }
58 |
59 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
60 | super.onViewCreated(view, savedInstanceState)
61 | delegate.onViewCreated(savedInstanceState)
62 | }
63 |
64 | override fun onActivityCreated(savedInstanceState: Bundle?) {
65 | super.onActivityCreated(savedInstanceState)
66 | delegate.onActivityCreated(savedInstanceState)
67 | }
68 |
69 | override fun onStart() {
70 | super.onStart()
71 | delegate.onStart()
72 | }
73 |
74 | override fun onResume() {
75 | super.onResume()
76 | delegate.onResume()
77 | }
78 |
79 | override fun onSaveInstanceState(outState: Bundle) {
80 | delegate.onSaveInstanceState(outState)
81 | super.onSaveInstanceState(outState)
82 | }
83 |
84 | override fun onPause() {
85 | delegate.onPause()
86 | super.onPause()
87 | }
88 |
89 | override fun onStop() {
90 | delegate.onStop()
91 | super.onStop()
92 | }
93 |
94 | override fun onDestroyView() {
95 | delegate.onDestroyView()
96 | super.onDestroyView()
97 | }
98 |
99 | override fun onDestroy() {
100 | delegate.onDestroy()
101 | super.onDestroy()
102 | }
103 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/base/PmController.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.base
27 |
28 | import android.os.Bundle
29 | import com.bluelinelabs.conductor.Controller
30 | import me.dmdev.rxpm.PmView
31 | import me.dmdev.rxpm.PresentationModel
32 | import me.dmdev.rxpm.delegate.PmControllerDelegate
33 |
34 | /**
35 | * Predefined [Conductor's Controller][Controller] implementing the [PmView][PmView].
36 | *
37 | * Just override the [providePresentationModel] and [onBindPresentationModel] methods and you are good to go.
38 | *
39 | * If extending is not possible you can implement [PmView],
40 | * create a [PmControllerDelegate] and pass the lifecycle callbacks to it.
41 | * See this class's source code for the example.
42 | */
43 | abstract class PmController(args: Bundle? = null) :
44 | Controller(args),
45 | PmView {
46 |
47 | @Suppress("LeakingThis")
48 | private val delegate = PmControllerDelegate(this)
49 |
50 | final override val presentationModel get() = delegate.presentationModel
51 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/base/PmDialogFragment.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.base
27 |
28 | import android.os.Bundle
29 | import android.view.View
30 | import androidx.appcompat.app.AppCompatDialogFragment
31 | import me.dmdev.rxpm.PmView
32 | import me.dmdev.rxpm.PresentationModel
33 | import me.dmdev.rxpm.delegate.PmFragmentDelegate
34 | import me.dmdev.rxpm.delegate.PmFragmentDelegate.RetainMode
35 |
36 | /**
37 | * Predefined [AppCompatDialogFragment] implementing the [PmView][PmView].
38 | *
39 | * Just override the [providePresentationModel] and [onBindPresentationModel] methods and you are good to go.
40 | *
41 | * If extending is not possible you can implement [PmView],
42 | * create a [PmFragmentDelegate] and pass the lifecycle callbacks to it.
43 | * See this class's source code for the example.
44 | */
45 | abstract class PmDialogFragment : AppCompatDialogFragment(), PmView {
46 |
47 | private val delegate by lazy(LazyThreadSafetyMode.NONE) {
48 | PmFragmentDelegate(this, RetainMode.CONFIGURATION_CHANGES)
49 | }
50 |
51 | final override val presentationModel get() = delegate.presentationModel
52 |
53 | override fun onCreate(savedInstanceState: Bundle?) {
54 | super.onCreate(savedInstanceState)
55 | delegate.onCreate(savedInstanceState)
56 | }
57 |
58 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
59 | super.onViewCreated(view, savedInstanceState)
60 | delegate.onViewCreated(savedInstanceState)
61 | }
62 |
63 | override fun onActivityCreated(savedInstanceState: Bundle?) {
64 | super.onActivityCreated(savedInstanceState)
65 | delegate.onActivityCreated(savedInstanceState)
66 | }
67 |
68 | override fun onStart() {
69 | super.onStart()
70 | delegate.onStart()
71 | }
72 |
73 | override fun onResume() {
74 | super.onResume()
75 | delegate.onResume()
76 | }
77 |
78 | override fun onSaveInstanceState(outState: Bundle) {
79 | delegate.onSaveInstanceState(outState)
80 | super.onSaveInstanceState(outState)
81 | }
82 |
83 | override fun onPause() {
84 | delegate.onPause()
85 | super.onPause()
86 | }
87 |
88 | override fun onStop() {
89 | delegate.onStop()
90 | super.onStop()
91 | }
92 |
93 | override fun onDestroyView() {
94 | delegate.onDestroyView()
95 | super.onDestroyView()
96 | }
97 |
98 | override fun onDestroy() {
99 | delegate.onDestroy()
100 | super.onDestroy()
101 | }
102 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/base/PmFragment.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.base
27 |
28 | import android.os.Bundle
29 | import android.view.View
30 | import androidx.fragment.app.Fragment
31 | import me.dmdev.rxpm.PmView
32 | import me.dmdev.rxpm.PresentationModel
33 | import me.dmdev.rxpm.delegate.PmFragmentDelegate
34 | import me.dmdev.rxpm.delegate.PmFragmentDelegate.RetainMode
35 |
36 | /**
37 | * Predefined [Fragment] implementing the [PmView][PmView].
38 | *
39 | * Just override the [providePresentationModel] and [onBindPresentationModel] methods and you are good to go.
40 | *
41 | * If extending is not possible you can implement [PmView],
42 | * create a [PmFragmentDelegate] and pass the lifecycle callbacks to it.
43 | * See this class's source code for the example.
44 | */
45 | abstract class PmFragment : Fragment(), PmView {
46 |
47 | private val delegate by lazy(LazyThreadSafetyMode.NONE) {
48 | PmFragmentDelegate(this, RetainMode.CONFIGURATION_CHANGES)
49 | }
50 |
51 | final override val presentationModel get() = delegate.presentationModel
52 |
53 | override fun onCreate(savedInstanceState: Bundle?) {
54 | super.onCreate(savedInstanceState)
55 | delegate.onCreate(savedInstanceState)
56 | }
57 |
58 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
59 | super.onViewCreated(view, savedInstanceState)
60 | delegate.onViewCreated(savedInstanceState)
61 | }
62 |
63 | override fun onActivityCreated(savedInstanceState: Bundle?) {
64 | super.onActivityCreated(savedInstanceState)
65 | delegate.onActivityCreated(savedInstanceState)
66 | }
67 |
68 | override fun onStart() {
69 | super.onStart()
70 | delegate.onStart()
71 | }
72 |
73 | override fun onResume() {
74 | super.onResume()
75 | delegate.onResume()
76 | }
77 |
78 | override fun onSaveInstanceState(outState: Bundle) {
79 | delegate.onSaveInstanceState(outState)
80 | super.onSaveInstanceState(outState)
81 | }
82 |
83 | override fun onPause() {
84 | delegate.onPause()
85 | super.onPause()
86 | }
87 |
88 | override fun onStop() {
89 | delegate.onStop()
90 | super.onStop()
91 | }
92 |
93 | override fun onDestroyView() {
94 | delegate.onDestroyView()
95 | super.onDestroyView()
96 | }
97 |
98 | override fun onDestroy() {
99 | delegate.onDestroy()
100 | super.onDestroy()
101 | }
102 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/delegate/CommonDelegate.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.delegate
27 |
28 | import android.os.Bundle
29 | import me.dmdev.rxpm.PmView
30 | import me.dmdev.rxpm.PresentationModel
31 | import me.dmdev.rxpm.PresentationModel.Lifecycle
32 | import me.dmdev.rxpm.bindTo
33 | import me.dmdev.rxpm.navigation.NavigationMessageDispatcher
34 | import me.dmdev.rxpm.navigation.NavigationalPm
35 | import java.util.*
36 |
37 | /**
38 | * Common delegate serves for forwarding the lifecycle[PresentationModel.Lifecycle] directly into the [PresentationModel][PresentationModel].
39 | * Can be used to implement your own delegate for the View[PmView].
40 | *
41 | * @see PmActivityDelegate
42 | * @see PmFragmentDelegate
43 | * @see PmControllerDelegate
44 | */
45 | class CommonDelegate(
46 | private val pmView: PmView,
47 | private val navigationMessagesDispatcher: NavigationMessageDispatcher
48 | )
49 | where PM : PresentationModel,
50 | V : PmView {
51 |
52 | companion object {
53 | private const val SAVED_PM_TAG_KEY = "_rxpm_presentation_model_tag"
54 | }
55 |
56 | private lateinit var pmTag: String
57 |
58 | val presentationModel: PM by lazy(LazyThreadSafetyMode.NONE) {
59 | @Suppress("UNCHECKED_CAST")
60 | PmStore.getPm(pmTag) { pmView.providePresentationModel() } as PM
61 | }
62 |
63 | fun onCreate(savedInstanceState: Bundle?) {
64 | pmTag = savedInstanceState?.getString(SAVED_PM_TAG_KEY) ?: UUID.randomUUID().toString()
65 | if (presentationModel.currentLifecycleState == null) {
66 | presentationModel.lifecycleConsumer.accept(Lifecycle.CREATED)
67 | }
68 | }
69 |
70 | fun onBind() {
71 |
72 | val pm = presentationModel
73 |
74 | if (pm.currentLifecycleState == Lifecycle.CREATED
75 | || pm.currentLifecycleState == Lifecycle.UNBINDED
76 | ) {
77 | pm.lifecycleConsumer.accept(Lifecycle.BINDED)
78 | pmView.onBindPresentationModel(pm)
79 |
80 | if (pm is NavigationalPm) {
81 | pm.navigationMessages bindTo {
82 | navigationMessagesDispatcher.dispatch(it)
83 | }
84 | }
85 | }
86 | }
87 |
88 | fun onResume() {
89 | if (presentationModel.currentLifecycleState == Lifecycle.BINDED
90 | || presentationModel.currentLifecycleState == Lifecycle.PAUSED
91 | ) {
92 | presentationModel.lifecycleConsumer.accept(Lifecycle.RESUMED)
93 | }
94 | }
95 |
96 | fun onSaveInstanceState(outState: Bundle) {
97 | outState.putString(SAVED_PM_TAG_KEY, pmTag)
98 | }
99 |
100 | fun onPause() {
101 | if (presentationModel.currentLifecycleState == Lifecycle.RESUMED) {
102 | presentationModel.lifecycleConsumer.accept(Lifecycle.PAUSED)
103 | }
104 | }
105 |
106 | fun onUnbind() {
107 | if (presentationModel.currentLifecycleState == Lifecycle.PAUSED
108 | || presentationModel.currentLifecycleState == Lifecycle.BINDED
109 | ) {
110 | pmView.onUnbindPresentationModel()
111 | presentationModel.lifecycleConsumer.accept(Lifecycle.UNBINDED)
112 | }
113 | }
114 |
115 | fun onDestroy() {
116 | if (presentationModel.currentLifecycleState == Lifecycle.CREATED
117 | || presentationModel.currentLifecycleState == Lifecycle.UNBINDED
118 | ) {
119 | PmStore.removePm(pmTag)
120 | presentationModel.lifecycleConsumer.accept(Lifecycle.DESTROYED)
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/delegate/PmActivityDelegate.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.delegate
27 |
28 | import android.app.Activity
29 | import android.os.Bundle
30 | import me.dmdev.rxpm.PmView
31 | import me.dmdev.rxpm.PresentationModel
32 | import me.dmdev.rxpm.base.PmActivity
33 | import me.dmdev.rxpm.delegate.PmActivityDelegate.RetainMode.CONFIGURATION_CHANGES
34 | import me.dmdev.rxpm.delegate.PmActivityDelegate.RetainMode.IS_FINISHING
35 | import me.dmdev.rxpm.navigation.ActivityNavigationMessageDispatcher
36 |
37 | /**
38 | * Delegate for the [Activity] that helps with creation and binding of
39 | * a [presentation model][PresentationModel] and a [view][PmView].
40 | *
41 | * Use this class only if you can't subclass the [PmActivity].
42 | *
43 | * Users of this class must forward all the lifecycle methods from the containing Activity
44 | * to the corresponding ones in this class.
45 | */
46 | class PmActivityDelegate(
47 | private val pmActivity: A,
48 | private val retainMode: RetainMode
49 | )
50 | where PM : PresentationModel,
51 | A : Activity, A : PmView {
52 |
53 | /**
54 | * Strategies for retaining the PresentationModel[PresentationModel].
55 | * [IS_FINISHING] - the PresentationModel will be destroyed if the Activity is finishing.
56 | * [CONFIGURATION_CHANGES] - Retain the PresentationModel during a configuration change.
57 | */
58 | enum class RetainMode { IS_FINISHING, CONFIGURATION_CHANGES }
59 |
60 | private val commonDelegate = CommonDelegate(pmActivity, ActivityNavigationMessageDispatcher(pmActivity))
61 |
62 | val presentationModel: PM get() = commonDelegate.presentationModel
63 |
64 | /**
65 | * You must call this method from the containing [Activity]'s corresponding method.
66 | */
67 | fun onCreate(savedInstanceState: Bundle?) {
68 | commonDelegate.onCreate(savedInstanceState)
69 | }
70 |
71 | /**
72 | * You must call this method from the containing [Activity]'s corresponding method.
73 | */
74 | fun onPostCreate() {
75 | commonDelegate.onBind()
76 | }
77 |
78 | /**
79 | * You must call this method from the containing [Activity]'s corresponding method.
80 | */
81 | fun onStart() {
82 | // For symmetry, may be used in the future
83 | }
84 |
85 | /**
86 | * You must call this method from the containing [Activity]'s corresponding method.
87 | */
88 | fun onResume() {
89 | commonDelegate.onResume()
90 | }
91 |
92 | /**
93 | * You must call this method from the containing [Activity]'s corresponding method.
94 | */
95 | fun onSaveInstanceState(outState: Bundle) {
96 | commonDelegate.onSaveInstanceState(outState)
97 | commonDelegate.onPause()
98 | }
99 |
100 | /**
101 | * You must call this method from the containing [Activity]'s corresponding method.
102 | */
103 | fun onPause() {
104 | commonDelegate.onPause()
105 | }
106 |
107 | /**
108 | * You must call this method from the containing [Activity]'s corresponding method.
109 | */
110 | fun onStop() {
111 | // For symmetry, may be used in the future
112 | }
113 |
114 | /**
115 | * You must call this method from the containing [Activity]'s corresponding method.
116 | */
117 | fun onDestroy() {
118 | commonDelegate.onUnbind()
119 |
120 | when (retainMode) {
121 | IS_FINISHING -> {
122 | if (pmActivity.isFinishing) {
123 | commonDelegate.onDestroy()
124 | }
125 | }
126 |
127 | CONFIGURATION_CHANGES -> {
128 | if (!pmActivity.isChangingConfigurations) {
129 | commonDelegate.onDestroy()
130 | }
131 | }
132 | }
133 | }
134 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/delegate/PmControllerDelegate.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.delegate
27 |
28 | import android.view.View
29 | import com.bluelinelabs.conductor.Controller
30 | import me.dmdev.rxpm.PmView
31 | import me.dmdev.rxpm.PresentationModel
32 | import me.dmdev.rxpm.base.PmController
33 | import me.dmdev.rxpm.navigation.ControllerNavigationMessageDispatcher
34 |
35 | /**
36 | * Delegate for the [Controller] that helps with creation and binding of
37 | * a [presentation model][PresentationModel] and a [view][PmView].
38 | *
39 | * Use this class only if you can't subclass the [PmController].
40 | *
41 | * Users of this class must forward all the life cycle methods from the containing Controller
42 | * to the corresponding ones in this class.
43 | */
44 | class PmControllerDelegate(pmController: C)
45 | where PM : PresentationModel,
46 | C : Controller, C : PmView {
47 |
48 | private var created = false
49 |
50 | private val commonDelegate =
51 | CommonDelegate(pmController, ControllerNavigationMessageDispatcher(pmController))
52 |
53 | val presentationModel: PM get() = commonDelegate.presentationModel
54 |
55 | init {
56 | pmController.addLifecycleListener(object : Controller.LifecycleListener() {
57 |
58 | override fun preCreateView(controller: Controller) {
59 | if (!created) {
60 | commonDelegate.onCreate(null)
61 | created = true
62 | }
63 | }
64 |
65 | override fun postCreateView(controller: Controller, view: View) {
66 | commonDelegate.onBind()
67 | }
68 |
69 | override fun postAttach(controller: Controller, view: View) {
70 | commonDelegate.onResume()
71 | }
72 |
73 | override fun preDetach(controller: Controller, view: View) {
74 | commonDelegate.onPause()
75 | }
76 |
77 | override fun preDestroyView(controller: Controller, view: View) {
78 | commonDelegate.onUnbind()
79 | }
80 |
81 | override fun preDestroy(controller: Controller) {
82 | if (created) {
83 | commonDelegate.onDestroy()
84 | }
85 | }
86 | })
87 | }
88 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/delegate/PmStore.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.delegate
27 |
28 | import me.dmdev.rxpm.PresentationModel
29 |
30 | internal object PmStore {
31 |
32 | private val pmMap = mutableMapOf()
33 |
34 | fun getPm(key: String, pmProvider: () -> PresentationModel): PresentationModel {
35 | return pmMap[key] ?: pmProvider().also { pmMap[key] = it }
36 | }
37 |
38 | fun removePm(key: String): PresentationModel? {
39 | return pmMap.remove(key)
40 | }
41 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/navigation/ActivityNavigationMessageDispatcher.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.navigation
27 |
28 | import android.app.Activity
29 |
30 | class ActivityNavigationMessageDispatcher(
31 | activity: Activity
32 | ) : NavigationMessageDispatcher(activity) {
33 |
34 | override fun getParent(node: Any?): Any? = null
35 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/navigation/ControllerNavigationMessageDispatcher.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.navigation
27 |
28 | import com.bluelinelabs.conductor.Controller
29 |
30 | class ControllerNavigationMessageDispatcher(
31 | controller: Controller
32 | ) : NavigationMessageDispatcher(controller) {
33 |
34 | override fun getParent(node: Any?): Any? {
35 | return if (node is Controller) {
36 | node.parentController ?: node.activity
37 | } else {
38 | null
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/navigation/FragmentNavigationMessageDispatcher.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.navigation
27 |
28 | import androidx.fragment.app.Fragment
29 |
30 | class FragmentNavigationMessageDispatcher(
31 | fragment: Fragment
32 | ) : NavigationMessageDispatcher(fragment) {
33 |
34 | override fun getParent(node: Any?): Any? {
35 | return if (node is Fragment) {
36 | node.parentFragment ?: node.activity
37 | } else {
38 | null
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/navigation/NavigationMessage.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.navigation
27 |
28 | /**
29 | * Marker interface for navigation message.
30 | * @see NavigationMessageHandler
31 | */
32 | interface NavigationMessage
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/navigation/NavigationMessageDispatcher.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.navigation
27 |
28 | abstract class NavigationMessageDispatcher(private val firstNode: Any) {
29 |
30 | fun dispatch(message: NavigationMessage) {
31 |
32 | var node: Any? = firstNode
33 |
34 | do {
35 | if (node is NavigationMessageHandler && node.handleNavigationMessage(message)) {
36 | return
37 | }
38 | node = getParent(node)
39 | } while (node != null)
40 |
41 | throw NotHandledNavigationMessageException()
42 | }
43 |
44 | abstract fun getParent(node: Any?): Any?
45 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/navigation/NavigationMessageHandler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.navigation
27 |
28 | /**
29 | * Interface for classes which implement navigation in the app.
30 | *
31 | * [Navigation messages][NavigationMessage] are dispatched up the hierarchy tree from child to parent
32 | * (e.g. from Fragment to it's parent Fragment and then to the Activity).
33 | * Any class in the chain that implements the interface can intercept the message and handle it.
34 | * If [handleNavigationMessage] returns true, the message will be treated as consumed and will not go further.
35 | */
36 | interface NavigationMessageHandler {
37 |
38 | /**
39 | * Handles the [navigation message][NavigationMessage].
40 | * @param message the navigation message.
41 | * @return true if [message] was handled, false otherwise.
42 | */
43 | fun handleNavigationMessage(message: NavigationMessage): Boolean
44 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/navigation/NavigationalPm.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.navigation
27 |
28 | import me.dmdev.rxpm.Command
29 |
30 | interface NavigationalPm {
31 |
32 | /**
33 | * Command to send [navigation message][NavigationMessage] to the [NavigationMessageHandler].
34 | */
35 | val navigationMessages: Command
36 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/navigation/NotHandledNavigationMessageException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.navigation
27 |
28 | /**
29 | * Thrown when there is no [NavigationMessageHandler] to handle the [navigation message][NavigationMessage].
30 | */
31 | class NotHandledNavigationMessageException
32 | : RuntimeException("You have no NavigationMessagesHandler to handle the message. Forgot to add?")
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/util/BufferSingleValueWhileIdleOperator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.util
27 |
28 | import io.reactivex.Observable
29 | import io.reactivex.ObservableOperator
30 | import io.reactivex.Observer
31 | import io.reactivex.disposables.CompositeDisposable
32 | import io.reactivex.disposables.Disposable
33 | import io.reactivex.plugins.RxJavaPlugins
34 |
35 | internal class BufferSingleValueWhileIdleOperator(
36 | private val idleObserver: Observable
37 | ) : ObservableOperator {
38 |
39 | override fun apply(observer: Observer): Observer {
40 | return ObserverWithBuffer(idleObserver, observer)
41 | }
42 |
43 | class ObserverWithBuffer(
44 | private val idleObserver: Observable,
45 | private val downstream: Observer
46 | ) : Observer {
47 |
48 | private val compositeDisposable = CompositeDisposable()
49 | private var done = false
50 |
51 | private var isIdle = false
52 | private var bufferedValue: T? = null
53 |
54 | override fun onSubscribe(disposable: Disposable) {
55 |
56 | compositeDisposable.addAll(
57 | disposable,
58 | idleObserver.subscribe {
59 | if (it) {
60 | isIdle = true
61 | } else {
62 | isIdle = false
63 | bufferedValue?.let { value ->
64 | onNext(value)
65 | }
66 | bufferedValue = null
67 | }
68 | }
69 | )
70 |
71 | downstream.onSubscribe(compositeDisposable)
72 | }
73 |
74 | override fun onNext(v: T) {
75 | if (done) {
76 | return
77 | }
78 |
79 | if (isIdle) {
80 | bufferedValue = v
81 | } else {
82 | downstream.onNext(v)
83 | }
84 | }
85 |
86 | override fun onError(e: Throwable) {
87 | if (done) {
88 | RxJavaPlugins.onError(e)
89 | return
90 | }
91 | done = true
92 | compositeDisposable.dispose()
93 | downstream.onError(e)
94 | }
95 |
96 | override fun onComplete() {
97 | if (done) {
98 | return
99 | }
100 |
101 | done = true
102 | compositeDisposable.dispose()
103 | downstream.onComplete()
104 | }
105 | }
106 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/util/BufferWhileIdleOperator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.util
27 |
28 | import io.reactivex.Observable
29 | import io.reactivex.ObservableOperator
30 | import io.reactivex.Observer
31 | import io.reactivex.disposables.CompositeDisposable
32 | import io.reactivex.disposables.Disposable
33 | import io.reactivex.plugins.RxJavaPlugins
34 | import java.util.*
35 |
36 | internal class BufferWhileIdleOperator(
37 | private val idleObserver: Observable,
38 | private val bufferSize: Int? = null
39 | ) : ObservableOperator {
40 |
41 | override fun apply(observer: Observer): Observer {
42 | return ObserverWithBuffer(idleObserver, observer, bufferSize)
43 | }
44 |
45 | class ObserverWithBuffer(
46 | private val idleObserver: Observable,
47 | private val downstream: Observer,
48 | private val bufferSize: Int? = null
49 | ) : Observer {
50 |
51 | private val compositeDisposable = CompositeDisposable()
52 | private var done = false
53 |
54 | private var isIdle = false
55 | private var bufferedValues: Queue = LinkedList()
56 |
57 | override fun onSubscribe(disposable: Disposable) {
58 | compositeDisposable.addAll(
59 | disposable,
60 | idleObserver.subscribe {
61 | if (it) {
62 | isIdle = true
63 | } else {
64 | isIdle = false
65 | bufferedValues.forEach { v ->
66 | onNext(v)
67 | }
68 | bufferedValues.clear()
69 | }
70 | }
71 | )
72 |
73 | downstream.onSubscribe(compositeDisposable)
74 | }
75 |
76 | override fun onNext(v: T) {
77 | if (done) {
78 | return
79 | }
80 |
81 | if (isIdle) {
82 |
83 | if (bufferedValues.size == bufferSize) {
84 | bufferedValues.poll()
85 | }
86 |
87 | bufferedValues.offer(v)
88 |
89 | } else {
90 | downstream.onNext(v)
91 | }
92 | }
93 |
94 | override fun onError(e: Throwable) {
95 | if (done) {
96 | RxJavaPlugins.onError(e)
97 | return
98 | }
99 | done = true
100 | compositeDisposable.dispose()
101 | downstream.onError(e)
102 | }
103 |
104 | override fun onComplete() {
105 | if (done) {
106 | return
107 | }
108 |
109 | done = true
110 | compositeDisposable.dispose()
111 | downstream.onComplete()
112 | }
113 | }
114 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/validation/CheckValidator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.validation
27 |
28 |
29 | /**
30 | * Implements [Validator] that uses a predefined [check condition][validation].
31 | * If the condition is invalid, then [doOnInvalid] is called.
32 | *
33 | * @see InputValidator
34 | * @see FormValidator
35 | */
36 | class CheckValidator internal constructor(
37 | private val validation: () -> Boolean,
38 | private val doOnInvalid: () -> Unit
39 | ) : Validator {
40 |
41 | override fun validate(): Boolean {
42 | val isValid = validation()
43 |
44 | if (!isValid) {
45 | doOnInvalid()
46 | }
47 |
48 | return isValid
49 | }
50 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/validation/FormValidator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.validation
27 |
28 | import me.dmdev.rxpm.PresentationModel
29 | import me.dmdev.rxpm.widget.CheckControl
30 | import me.dmdev.rxpm.widget.InputControl
31 |
32 |
33 | /**
34 | * Use this class to validate the form.
35 | * To check the [input fields][input] and [checkbox][check] [create][formValidator] FormValidator using DSL.
36 | * Also you can create your own validators by analogy and use them together.
37 | *
38 | * @see InputValidator
39 | * @see CheckValidator
40 | */
41 | class FormValidator internal constructor(): PresentationModel(), Validator {
42 |
43 | private val validators = mutableListOf()
44 |
45 | /**
46 | * Adds a [validator].
47 | */
48 | fun addValidator(validator: Validator) {
49 | validators.add(validator)
50 | }
51 |
52 | override fun validate(): Boolean {
53 | var isFormValid = true
54 | validators.forEach { validator ->
55 | val isValid = validator.validate()
56 |
57 | if (!isValid) {
58 | isFormValid = false
59 | }
60 | }
61 | return isFormValid
62 | }
63 |
64 | override fun onCreate() {
65 |
66 | validators.forEach { validator ->
67 | if (validator is InputValidator && validator.validateOnFocusLoss) {
68 | validator.inputControl.focus.observable
69 | .skip(1)
70 | .filter { hasFocus -> !hasFocus }
71 | .subscribe {
72 | validator.validate()
73 | }
74 | .untilDestroy()
75 | }
76 | }
77 | }
78 | }
79 |
80 | /**
81 | * Creates the [FormValidator]. Add [input][input] and [check][check] validators in [init].
82 | */
83 | @Suppress("unused")
84 | fun PresentationModel.formValidator(init: FormValidator.() -> Unit): FormValidator {
85 | val formValidator = FormValidator()
86 | formValidator.init()
87 | return formValidator.apply {
88 | attachToParent(this@formValidator)
89 | }
90 | }
91 |
92 |
93 | /**
94 | * Creates the [InputValidator] for [inputControl] and adds it to the [FormValidator].
95 | */
96 | fun FormValidator.input(
97 | inputControl: InputControl,
98 | required: Boolean = true,
99 | validateOnFocusLoss: Boolean = false,
100 | init: InputValidator.() -> Unit
101 | ) {
102 | val inputValidator = InputValidator(inputControl, required, validateOnFocusLoss)
103 | inputValidator.init()
104 | addValidator(inputValidator)
105 | }
106 |
107 |
108 | /**
109 | * Creates the [CheckValidator] for [checkControl] and adds it to the [FormValidator].
110 | */
111 | fun FormValidator.check(
112 | checkControl: CheckControl,
113 | doOnInvalid: () -> Unit = {}
114 | ) {
115 | val checkValidator = CheckValidator(
116 | validation = { checkControl.checked.valueOrNull == true },
117 | doOnInvalid = doOnInvalid
118 | )
119 | addValidator(checkValidator)
120 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/validation/InputValidator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.validation
27 |
28 | import me.dmdev.rxpm.PresentationModel
29 | import me.dmdev.rxpm.widget.InputControl
30 |
31 |
32 | /**
33 | * Validates a text from [InputControl] and post an error text if it is invalid.
34 | * You can [add][addValidation] multiple validations. This class is not used directly,
35 | * create a form validator using [DSL][PresentationModel.formValidator] instead,
36 | * and add into it input checks such as [empty], [pattern] and other.
37 | *
38 | * @see CheckValidator
39 | * @see FormValidator
40 | */
41 | class InputValidator internal constructor(
42 | internal val inputControl: InputControl,
43 | private val required: Boolean,
44 | internal val validateOnFocusLoss: Boolean
45 | ) : Validator {
46 |
47 | private val validations = mutableListOf Boolean, String>>()
48 |
49 | /**
50 | * Adds a text validation.
51 | * @param validation - pair of validation and error text for [InputControl].
52 | */
53 | fun addValidation(validation: Pair<(String) -> Boolean, String>) {
54 | validations.add(validation)
55 | }
56 |
57 | override fun validate(): Boolean {
58 |
59 | if (inputControl.text.value.isBlank() && !required) {
60 | return true
61 | }
62 |
63 | validations.forEach { (predicate, errorMessage) ->
64 | if (!predicate(inputControl.text.value)) {
65 | inputControl.error.relay.accept(errorMessage)
66 | return false
67 | }
68 | }
69 |
70 | return true
71 | }
72 | }
73 |
74 | /**
75 | * Adds a check that the text is not empty.
76 | */
77 | fun InputValidator.empty(errorMessage: String) {
78 | addValidation(String::isNotEmpty to errorMessage)
79 | }
80 |
81 | /**
82 | * Adds a validation based on a [regular expression][regex].
83 | */
84 | fun InputValidator.pattern(regex: String, errorMessage: String) {
85 | addValidation(
86 | { str: String -> regex.toRegex().matches(str) } to errorMessage
87 | )
88 | }
89 |
90 | /**
91 | * Adds a custom condition to check the text.
92 | */
93 | fun InputValidator.valid(validation: (input: String) -> Boolean, errorMessage: String) {
94 | addValidation(validation to errorMessage)
95 | }
96 |
97 | /**
98 | * Adds a check for a minimum [number] of symbols.
99 | */
100 | fun InputValidator.minSymbols(number: Int, errorMessage: String) {
101 | addValidation({ str: String -> str.length >= number } to errorMessage)
102 | }
103 |
104 | /**
105 | * Adds a check that the text from another [input] is the same.
106 | * Used for example to confirm password entry.
107 | */
108 | fun InputValidator.equalsTo(input: InputControl, errorMessage: String) {
109 | addValidation({ str: String -> str == input.text.valueOrNull } to errorMessage)
110 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/validation/Validator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.validation
27 |
28 |
29 | /**
30 | * Interface used to define whether a condition is satisfied.
31 | */
32 | interface Validator {
33 |
34 | /**
35 | * Runs condition check.
36 | *
37 | * @return true if condition is valid, false otherwise.
38 | */
39 | fun validate(): Boolean
40 | }
--------------------------------------------------------------------------------
/rxpm/src/main/kotlin/me/dmdev/rxpm/widget/CheckControl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.widget
27 |
28 | import android.widget.*
29 | import com.jakewharton.rxbinding3.widget.*
30 | import me.dmdev.rxpm.*
31 |
32 | /**
33 | * Helps to bind a group of properties of a checkable widget to a [presentation model][PresentationModel]
34 | * and also breaks the loop of two-way data binding to make the work with the check easier.
35 | *
36 | * You can bind this to any [CompoundButton] subclass using the [bindTo][bindTo] extension.
37 | *
38 | * Instantiate this using the [checkControl] extension function of the presentation model.
39 | *
40 | * @see InputControl
41 | * @see DialogControl
42 | */
43 | class CheckControl internal constructor(initialChecked: Boolean) : PresentationModel() {
44 |
45 | /**
46 | * The checked [state][State].
47 | */
48 | val checked = state(initialChecked)
49 |
50 | /**
51 | * The checked state change [events][Action].
52 | */
53 | val checkedChanges = action()
54 |
55 | override fun onCreate() {
56 | super.onCreate()
57 | checkedChanges.observable
58 | .filter { it != checked.value }
59 | .subscribe(checked.consumer)
60 | .untilDestroy()
61 | }
62 | }
63 |
64 | /**
65 | * Creates the [CheckControl].
66 | *
67 | * @param initialChecked initial checked state.
68 | */
69 | fun PresentationModel.checkControl(initialChecked: Boolean = false): CheckControl {
70 | return CheckControl(initialChecked).apply {
71 | attachToParent(this@checkControl)
72 | }
73 | }
74 |
75 | /**
76 | * Binds the [CheckControl] to the [CompoundButton][compoundButton], use it ONLY in [PmView.onBindPresentationModel].
77 | */
78 | infix fun CheckControl.bindTo(compoundButton: CompoundButton) {
79 |
80 | var editing = false
81 |
82 | checked bindTo {
83 | editing = true
84 | compoundButton.isChecked = it
85 | editing = false
86 | }
87 |
88 | compoundButton.checkedChanges()
89 | .skipInitialValue()
90 | .filter { !editing && it != checked.value }
91 | .bindTo(checkedChanges)
92 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/delegate/CommonDelegateTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.delegate
27 |
28 | import com.nhaarman.mockitokotlin2.*
29 | import io.reactivex.disposables.CompositeDisposable
30 | import me.dmdev.rxpm.PmView
31 | import me.dmdev.rxpm.PresentationModel
32 | import me.dmdev.rxpm.navigation.NavigationMessageDispatcher
33 | import me.dmdev.rxpm.util.SchedulersRule
34 | import org.junit.Before
35 | import org.junit.Rule
36 | import org.junit.Test
37 | import kotlin.test.assertEquals
38 |
39 | class CommonDelegateTest {
40 |
41 | @get:Rule val schedulers = SchedulersRule()
42 |
43 | private lateinit var pm: PresentationModel
44 | private lateinit var compositeDisposable: CompositeDisposable
45 | private lateinit var view: PmView
46 | private lateinit var navigationMessagesDispatcher: NavigationMessageDispatcher
47 | private lateinit var delegate: CommonDelegate>
48 |
49 | @Before fun setUp() {
50 | pm = spy()
51 | compositeDisposable = mock()
52 | view = mockView()
53 | navigationMessagesDispatcher = mock()
54 | delegate = CommonDelegate(view, navigationMessagesDispatcher)
55 | }
56 |
57 | private fun mockView(): PmView {
58 | return mock {
59 | on { providePresentationModel() } doReturn pm
60 | }
61 | }
62 |
63 | @Test fun callViewMethods() {
64 |
65 | verify(view, never()).providePresentationModel()
66 | delegate.onCreate(null)
67 | verify(view).providePresentationModel()
68 | assertEquals(pm, delegate.presentationModel)
69 |
70 | verify(view, never()).onBindPresentationModel(pm)
71 | delegate.onBind()
72 | verify(view).onBindPresentationModel(pm)
73 |
74 | delegate.onResume()
75 | delegate.onPause()
76 |
77 | verify(view, never()).onUnbindPresentationModel()
78 | delegate.onUnbind()
79 | verify(view).onUnbindPresentationModel()
80 |
81 | delegate.onDestroy()
82 |
83 | verify(view, times(1)).onBindPresentationModel(pm)
84 | verify(view, times(1)).onUnbindPresentationModel()
85 | verify(view, times(1)).onUnbindPresentationModel()
86 | }
87 |
88 | @Test fun changePmLifecycle() {
89 |
90 | val testObserver = pm.lifecycleObservable.test()
91 |
92 | delegate.onCreate(null)
93 | delegate.onBind()
94 | delegate.onResume()
95 | delegate.onPause()
96 | delegate.onUnbind()
97 | delegate.onDestroy()
98 |
99 | testObserver.assertValuesOnly(
100 | PresentationModel.Lifecycle.CREATED,
101 | PresentationModel.Lifecycle.BINDED,
102 | PresentationModel.Lifecycle.RESUMED,
103 | PresentationModel.Lifecycle.PAUSED,
104 | PresentationModel.Lifecycle.UNBINDED,
105 | PresentationModel.Lifecycle.DESTROYED
106 | )
107 | }
108 |
109 | @Test fun filterRepeatedLifecycleCalls() {
110 |
111 | val testObserver = pm.lifecycleObservable.test()
112 |
113 | delegate.onCreate(null)
114 | delegate.onCreate(null)
115 | delegate.onBind()
116 | delegate.onBind()
117 | delegate.onResume()
118 | delegate.onResume()
119 | delegate.onPause()
120 | delegate.onPause()
121 | delegate.onUnbind()
122 | delegate.onUnbind()
123 | delegate.onDestroy()
124 | delegate.onDestroy()
125 |
126 | testObserver.assertValuesOnly(
127 | PresentationModel.Lifecycle.CREATED,
128 | PresentationModel.Lifecycle.BINDED,
129 | PresentationModel.Lifecycle.RESUMED,
130 | PresentationModel.Lifecycle.PAUSED,
131 | PresentationModel.Lifecycle.UNBINDED,
132 | PresentationModel.Lifecycle.DESTROYED
133 | )
134 | }
135 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/delegate/PmActivityDelegateTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.delegate
27 |
28 | import com.nhaarman.mockitokotlin2.*
29 | import io.reactivex.disposables.CompositeDisposable
30 | import me.dmdev.rxpm.PresentationModel
31 | import me.dmdev.rxpm.PresentationModel.Lifecycle.*
32 | import me.dmdev.rxpm.base.PmActivity
33 | import me.dmdev.rxpm.delegate.PmActivityDelegate.RetainMode
34 | import me.dmdev.rxpm.util.SchedulersRule
35 | import org.junit.Before
36 | import org.junit.Rule
37 | import org.junit.Test
38 | import kotlin.test.assertEquals
39 |
40 | class PmActivityDelegateTest {
41 |
42 | @get:Rule val schedulers = SchedulersRule()
43 |
44 | private lateinit var pm: PresentationModel
45 | private lateinit var compositeDisposable: CompositeDisposable
46 | private lateinit var view: PmActivity
47 | private lateinit var delegate: PmActivityDelegate>
48 |
49 | @Before fun setUp() {
50 | pm = spy()
51 | compositeDisposable = mock()
52 | view = mockView()
53 |
54 | delegate = PmActivityDelegate(view, RetainMode.IS_FINISHING)
55 | }
56 |
57 | private fun mockView(): PmActivity {
58 | return mock {
59 | on { providePresentationModel() } doReturn pm
60 | }
61 | }
62 |
63 | @Test fun callViewMethods() {
64 | delegate.onCreate(null)
65 | delegate.onPostCreate()
66 |
67 | verify(view).providePresentationModel()
68 | assertEquals(pm, delegate.presentationModel)
69 | verify(view).onBindPresentationModel(pm)
70 |
71 | delegate.onStart()
72 | delegate.onResume()
73 | delegate.onPause()
74 | delegate.onStop()
75 |
76 | delegate.onDestroy()
77 | verify(view).onUnbindPresentationModel()
78 |
79 | }
80 |
81 | @Test fun changePmLifecycle() {
82 | val testObserver = pm.lifecycleObservable.test()
83 |
84 | delegate.onCreate(null)
85 | delegate.onPostCreate()
86 | delegate.onStart()
87 | delegate.onResume()
88 | delegate.onPause()
89 | delegate.onStop()
90 | whenever(view.isFinishing).thenReturn(true)
91 | delegate.onDestroy()
92 |
93 | testObserver.assertValuesOnly(
94 | CREATED,
95 | BINDED,
96 | RESUMED,
97 | PAUSED,
98 | UNBINDED,
99 | DESTROYED
100 | )
101 | }
102 |
103 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/delegate/PmControllerDelegateTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.delegate
27 |
28 | import android.view.View
29 | import com.bluelinelabs.conductor.Controller
30 | import com.nhaarman.mockitokotlin2.doReturn
31 | import com.nhaarman.mockitokotlin2.mock
32 | import com.nhaarman.mockitokotlin2.spy
33 | import com.nhaarman.mockitokotlin2.verify
34 | import io.reactivex.disposables.CompositeDisposable
35 | import me.dmdev.rxpm.PresentationModel
36 | import me.dmdev.rxpm.PresentationModel.Lifecycle.*
37 | import me.dmdev.rxpm.base.PmController
38 | import me.dmdev.rxpm.util.SchedulersRule
39 | import org.junit.Before
40 | import org.junit.Rule
41 | import org.junit.Test
42 | import org.mockito.ArgumentCaptor
43 | import kotlin.test.assertEquals
44 |
45 |
46 | class PmControllerDelegateTest {
47 |
48 | @get:Rule val schedulers = SchedulersRule()
49 |
50 | private lateinit var pm: PresentationModel
51 | private lateinit var compositeDisposable: CompositeDisposable
52 | private lateinit var pmController: PmController
53 | private lateinit var view: View
54 | private lateinit var delegate: PmControllerDelegate>
55 | private lateinit var controllerLifecycleListener: Controller.LifecycleListener
56 |
57 | @Before fun setUp() {
58 | pm = spy()
59 | compositeDisposable = mock()
60 | pmController = mockPmController()
61 | view = mock()
62 | delegate = PmControllerDelegate(pmController)
63 | controllerLifecycleListener = captureControllerLifecycleListener()
64 | }
65 |
66 | private fun captureControllerLifecycleListener(): Controller.LifecycleListener {
67 | val argument = ArgumentCaptor.forClass(Controller.LifecycleListener::class.java)
68 | verify(pmController).addLifecycleListener(argument.capture())
69 | return argument.value
70 | }
71 |
72 | private fun mockPmController(): PmController {
73 | return mock {
74 | on { providePresentationModel() } doReturn pm
75 | }
76 | }
77 |
78 | @Test fun callViewMethods() {
79 |
80 | controllerLifecycleListener.preCreateView(pmController)
81 |
82 | verify(pmController).providePresentationModel()
83 | assertEquals(pm, delegate.presentationModel)
84 |
85 | controllerLifecycleListener.postCreateView(pmController, view)
86 |
87 | verify(pmController).onBindPresentationModel(pm)
88 |
89 | controllerLifecycleListener.preAttach(pmController, view)
90 | controllerLifecycleListener.postAttach(pmController, view)
91 | controllerLifecycleListener.preDetach(pmController, view)
92 | controllerLifecycleListener.postDetach(pmController, view)
93 | controllerLifecycleListener.preDestroyView(pmController, view)
94 |
95 | verify(pmController).onUnbindPresentationModel()
96 |
97 | controllerLifecycleListener.postDestroyView(pmController)
98 | controllerLifecycleListener.preDestroy(pmController)
99 | controllerLifecycleListener.postDestroy(pmController)
100 |
101 | }
102 |
103 | @Test fun changePmLifecycle() {
104 | val testObserver = pm.lifecycleObservable.test()
105 |
106 | controllerLifecycleListener.preCreateView(pmController)
107 | controllerLifecycleListener.postCreateView(pmController, view)
108 | controllerLifecycleListener.preAttach(pmController, view)
109 | controllerLifecycleListener.postAttach(pmController, view)
110 | controllerLifecycleListener.preDetach(pmController, view)
111 | controllerLifecycleListener.postDetach(pmController, view)
112 | controllerLifecycleListener.preDestroyView(pmController, view)
113 | controllerLifecycleListener.postDestroyView(pmController)
114 | controllerLifecycleListener.preDestroy(pmController)
115 | controllerLifecycleListener.postDestroy(pmController)
116 |
117 | testObserver.assertValuesOnly(
118 | CREATED,
119 | BINDED,
120 | RESUMED,
121 | PAUSED,
122 | UNBINDED,
123 | DESTROYED
124 | )
125 | }
126 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/delegate/PmFragmentDelegateTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.delegate
27 |
28 | import androidx.fragment.app.FragmentActivity
29 | import com.nhaarman.mockitokotlin2.*
30 | import io.reactivex.disposables.CompositeDisposable
31 | import me.dmdev.rxpm.PresentationModel
32 | import me.dmdev.rxpm.PresentationModel.Lifecycle.*
33 | import me.dmdev.rxpm.base.PmFragment
34 | import me.dmdev.rxpm.delegate.PmFragmentDelegate.RetainMode
35 | import me.dmdev.rxpm.util.SchedulersRule
36 | import org.junit.Before
37 | import org.junit.Rule
38 | import org.junit.Test
39 | import kotlin.test.assertEquals
40 |
41 | class PmFragmentDelegateTest {
42 |
43 | @get:Rule val schedulers = SchedulersRule()
44 |
45 | private lateinit var pm: PresentationModel
46 | private lateinit var compositeDisposable: CompositeDisposable
47 | private lateinit var activity: FragmentActivity
48 | private lateinit var view: PmFragment
49 | private lateinit var delegate: PmFragmentDelegate>
50 |
51 | @Before fun setUp() {
52 | pm = spy()
53 | compositeDisposable = mock()
54 | activity = mock()
55 | view = mockView()
56 |
57 | delegate = PmFragmentDelegate(view, RetainMode.CONFIGURATION_CHANGES)
58 | }
59 |
60 | private fun mockView(): PmFragment {
61 | return mock {
62 | on { providePresentationModel() } doReturn pm
63 | on { activity } doReturn activity
64 | }
65 | }
66 |
67 | @Test fun callViewMethods() {
68 |
69 | delegate.onCreate(null)
70 | delegate.onViewCreated(null)
71 | delegate.onActivityCreated(null)
72 |
73 | verify(view).providePresentationModel()
74 | assertEquals(pm, delegate.presentationModel)
75 | verify(view).onBindPresentationModel(pm)
76 |
77 | delegate.onStart()
78 | delegate.onResume()
79 | delegate.onPause()
80 | delegate.onStop()
81 |
82 | delegate.onDestroyView()
83 | verify(view).onUnbindPresentationModel()
84 | delegate.onDestroy()
85 | }
86 |
87 | @Test fun changePmLifecycle() {
88 |
89 | val testObserver = pm.lifecycleObservable.test()
90 |
91 | delegate.onCreate(null)
92 | delegate.onViewCreated(null)
93 | delegate.onActivityCreated(null)
94 | delegate.onStart()
95 | delegate.onResume()
96 | delegate.onPause()
97 | delegate.onStop()
98 | delegate.onDestroyView()
99 | whenever(activity.isFinishing).thenReturn(true)
100 | delegate.onDestroy()
101 |
102 | testObserver.assertValuesOnly(
103 | CREATED,
104 | BINDED,
105 | RESUMED,
106 | PAUSED,
107 | UNBINDED,
108 | DESTROYED
109 | )
110 | }
111 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/navigation/NavigationMessageDispatcherTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.navigation
27 |
28 | import com.nhaarman.mockitokotlin2.*
29 | import org.junit.Before
30 | import org.junit.Test
31 | import kotlin.test.assertFailsWith
32 |
33 | class NavigationMessageDispatcherTest {
34 |
35 | private val testMessage = object : NavigationMessage {}
36 |
37 | private lateinit var workingHandler: NavigationMessageHandler
38 | private lateinit var ignoringHandler: NavigationMessageHandler
39 | private lateinit var unreachableHandler: NavigationMessageHandler
40 |
41 | @Before fun setUp() {
42 | workingHandler = mockMessageHandler(true)
43 | ignoringHandler = mockMessageHandler(false)
44 | unreachableHandler = mockMessageHandler(true)
45 | }
46 |
47 | private fun mockMessageHandler(handleMessages: Boolean): NavigationMessageHandler {
48 | return mock {
49 | on { handleNavigationMessage(any()) } doReturn handleMessages
50 | }
51 | }
52 |
53 | @Test fun handleMessage() {
54 | val dispatcher = createDispatcher(listOf(Unit, ignoringHandler, workingHandler, unreachableHandler))
55 |
56 | dispatcher.dispatch(testMessage)
57 |
58 | verify(ignoringHandler).handleNavigationMessage(testMessage)
59 | verify(workingHandler).handleNavigationMessage(testMessage)
60 | verifyZeroInteractions(unreachableHandler)
61 | }
62 |
63 | private fun createDispatcher(handlers: List): NavigationMessageDispatcher {
64 | return object : NavigationMessageDispatcher(Unit) {
65 |
66 | var k = 0
67 |
68 | override fun getParent(node: Any?): Any? {
69 |
70 | val result: Any? = try {
71 | handlers[k]
72 | } catch (e: ArrayIndexOutOfBoundsException) {
73 | null
74 | }
75 | k++
76 |
77 | return result
78 | }
79 | }
80 | }
81 |
82 | @Test fun failsIfMessageNotHandled() {
83 | val dispatcher = createDispatcher(listOf(Unit, ignoringHandler))
84 |
85 | assertFailsWith {
86 | dispatcher.dispatch(testMessage)
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/util/SchedulersRule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.util
27 |
28 | import io.reactivex.android.plugins.RxAndroidPlugins
29 | import io.reactivex.plugins.RxJavaPlugins
30 | import io.reactivex.schedulers.Schedulers
31 | import io.reactivex.schedulers.TestScheduler
32 | import org.junit.rules.ExternalResource
33 |
34 | class SchedulersRule(private val useTestScheduler: Boolean = false) : ExternalResource() {
35 |
36 | private lateinit var _testScheduler: TestScheduler
37 |
38 | val testScheduler: TestScheduler
39 | get() {
40 | check(useTestScheduler) { "TestScheduler is switched off." }
41 | return _testScheduler
42 | }
43 |
44 | override fun before() {
45 | RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
46 | RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
47 |
48 | val computationScheduler = if (useTestScheduler) {
49 | _testScheduler = TestScheduler()
50 | _testScheduler
51 | } else {
52 | Schedulers.trampoline()
53 | }
54 | RxJavaPlugins.setComputationSchedulerHandler { computationScheduler }
55 | }
56 |
57 | override fun after() {
58 | RxJavaPlugins.reset()
59 | RxAndroidPlugins.reset()
60 | }
61 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/validation/CheckValidatorTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.validation
27 |
28 | import com.nhaarman.mockitokotlin2.mock
29 | import com.nhaarman.mockitokotlin2.verify
30 | import com.nhaarman.mockitokotlin2.verifyZeroInteractions
31 | import org.junit.Test
32 | import kotlin.test.assertFalse
33 | import kotlin.test.assertTrue
34 |
35 |
36 | class CheckValidatorTest {
37 |
38 | @Test fun checkIsValid() {
39 |
40 | val doOnInvalid = mock<() -> Unit>()
41 |
42 | val checkValidator = CheckValidator(
43 | validation = { true },
44 | doOnInvalid = doOnInvalid
45 | )
46 |
47 | assertTrue(checkValidator.validate())
48 | verifyZeroInteractions(doOnInvalid)
49 | }
50 |
51 | @Test fun checkIsInvalid() {
52 |
53 | val doOnInvalid = mock<() -> Unit>()
54 |
55 | val checkValidator = CheckValidator(
56 | validation = { false },
57 | doOnInvalid = doOnInvalid
58 | )
59 |
60 | assertFalse(checkValidator.validate())
61 | verify(doOnInvalid).invoke()
62 | }
63 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/validation/FormValidatorTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.validation
27 |
28 | import com.nhaarman.mockitokotlin2.doReturn
29 | import com.nhaarman.mockitokotlin2.mock
30 | import com.nhaarman.mockitokotlin2.spy
31 | import com.nhaarman.mockitokotlin2.verify
32 | import me.dmdev.rxpm.PresentationModel
33 | import me.dmdev.rxpm.test.PmTestHelper
34 | import me.dmdev.rxpm.widget.inputControl
35 | import org.junit.Before
36 | import org.junit.Test
37 | import kotlin.test.assertEquals
38 | import kotlin.test.assertFalse
39 | import kotlin.test.assertNull
40 | import kotlin.test.assertTrue
41 |
42 | class FormValidatorTest {
43 |
44 | private val errorText = "error"
45 | private lateinit var pm : PresentationModel
46 | private lateinit var pmTestHelper: PmTestHelper
47 |
48 | @Before fun init() {
49 | pm = spy()
50 | pmTestHelper = PmTestHelper(pm)
51 | }
52 |
53 | @Test fun formIsValid() {
54 |
55 | val formValidator = pm.formValidator {}
56 |
57 | val validator1 = mock {
58 | onGeneric { validate() }.doReturn(true)
59 | }
60 |
61 | val validator2 = mock {
62 | onGeneric { validate() }.doReturn(true)
63 | }
64 |
65 | val validator3 = mock {
66 | onGeneric { validate() }.doReturn(true)
67 | }
68 |
69 | formValidator.addValidator(validator1)
70 | formValidator.addValidator(validator2)
71 | formValidator.addValidator(validator3)
72 |
73 | assertTrue(formValidator.validate())
74 |
75 | verify(validator1).validate()
76 | verify(validator2).validate()
77 | verify(validator3).validate()
78 |
79 | }
80 |
81 | @Test fun formIsInvalid() {
82 |
83 | val formValidator = pm.formValidator {}
84 |
85 | val validator1 = mock {
86 | onGeneric { validate() }.doReturn(true)
87 | }
88 |
89 | val validator2 = mock {
90 | onGeneric { validate() }.doReturn(false)
91 | }
92 |
93 | val validator3 = mock {
94 | onGeneric { validate() }.doReturn(true)
95 | }
96 |
97 | formValidator.addValidator(validator1)
98 | formValidator.addValidator(validator2)
99 | formValidator.addValidator(validator3)
100 |
101 | assertFalse(formValidator.validate())
102 |
103 | verify(validator1).validate()
104 | verify(validator2).validate()
105 | verify(validator3).validate()
106 |
107 | }
108 |
109 | @Test fun validateInputOnFocusLoss() {
110 |
111 | val inputControl = pm.inputControl("abc")
112 |
113 | val formValidator = pm.formValidator {
114 |
115 | input(
116 | inputControl = inputControl,
117 | required = true,
118 | validateOnFocusLoss = true
119 | ) {
120 | empty(errorText)
121 | }
122 | }
123 |
124 | pmTestHelper.setLifecycleTo(PresentationModel.Lifecycle.RESUMED)
125 | inputControl.focusChanges.consumer.accept(true)
126 | inputControl.text.relay.accept("")
127 |
128 | assertNull(inputControl.error.valueOrNull)
129 |
130 | inputControl.focusChanges.consumer.accept(false)
131 |
132 | assertEquals(errorText, inputControl.error.valueOrNull)
133 |
134 | }
135 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/widget/CheckControlTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.widget
27 |
28 | import me.dmdev.rxpm.PresentationModel
29 | import me.dmdev.rxpm.PresentationModel.Lifecycle.CREATED
30 | import me.dmdev.rxpm.test.PmTestHelper
31 | import org.junit.Test
32 |
33 | class CheckControlTest {
34 |
35 | @Test fun filterIfValueNotChanged() {
36 | val pm = object : PresentationModel() {}
37 | val pmTestHelper = PmTestHelper(pm)
38 |
39 | val checkbox = pm.checkControl()
40 |
41 | pmTestHelper.setLifecycleTo(CREATED)
42 |
43 | val testObserver = checkbox.checked.observable.test()
44 |
45 | checkbox.checkedChanges.consumer.run {
46 | accept(true)
47 | accept(true)
48 | accept(false)
49 | accept(false)
50 | accept(true)
51 | accept(false)
52 | }
53 |
54 | testObserver
55 | .assertValues(
56 | false, // initial value
57 | true,
58 | false,
59 | true,
60 | false
61 | )
62 | .assertNoErrors()
63 | }
64 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/widget/DialogControlTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.widget
27 |
28 | import me.dmdev.rxpm.PresentationModel
29 | import me.dmdev.rxpm.widget.DialogControl.Display.Absent
30 | import me.dmdev.rxpm.widget.DialogControl.Display.Displayed
31 | import org.junit.Before
32 | import org.junit.Test
33 | import kotlin.test.assertTrue
34 |
35 | class DialogControlTest {
36 |
37 | private lateinit var dialogControl: DialogControl
38 |
39 | @Before fun setUp() {
40 | dialogControl = createDialogControl()
41 | }
42 |
43 | private fun createDialogControl(): DialogControl {
44 | val pm = object : PresentationModel() {}
45 | return pm.dialogControl()
46 | }
47 |
48 | @Test fun displayedOnShow() {
49 | dialogControl.showForResult(Unit).subscribe()
50 | assertTrue { dialogControl.displayed.value is Displayed<*> }
51 | }
52 |
53 | @Test fun removedOnResult() {
54 | dialogControl.showForResult(Unit).subscribe()
55 | dialogControl.sendResult(Unit)
56 | assertTrue { dialogControl.displayed.value === Absent }
57 | }
58 |
59 | @Test fun acceptOneResult() {
60 | val testObserver = dialogControl.showForResult(Unit).test()
61 |
62 | // When two results sent
63 | dialogControl.sendResult(Unit)
64 | dialogControl.sendResult(Unit)
65 |
66 | // Then only one is here
67 | testObserver.assertResult(Unit)
68 | }
69 |
70 | @Test fun removedOnDismiss() {
71 | dialogControl.showForResult(Unit).subscribe()
72 | dialogControl.dismiss()
73 | assertTrue { dialogControl.displayed.value === Absent }
74 | }
75 |
76 | @Test fun cancelDialog() {
77 | val testObserver = dialogControl.showForResult(Unit).test()
78 | dialogControl.dismiss()
79 |
80 | testObserver
81 | .assertSubscribed()
82 | .assertNoValues()
83 | .assertNoErrors()
84 | .assertComplete()
85 | }
86 |
87 | @Test fun dismissPreviousOnNewShow() {
88 | val displayedObserver = dialogControl.displayed.observable.test()
89 |
90 | val firstObserver = dialogControl.showForResult(Unit).test()
91 | val secondObserver = dialogControl.showForResult(Unit).test()
92 |
93 | displayedObserver
94 | .assertSubscribed()
95 | .assertValueCount(4)
96 | .assertValueAt(0, Absent)
97 | .assertValueAt(1) { it is Displayed<*> }
98 | .assertValueAt(2, Absent)
99 | .assertValueAt(3) { it is Displayed<*> }
100 | .assertNoErrors()
101 |
102 | firstObserver
103 | .assertSubscribed()
104 | .assertNoValues()
105 | .assertNoErrors()
106 | .assertComplete()
107 |
108 | secondObserver
109 | .assertEmpty()
110 | }
111 | }
--------------------------------------------------------------------------------
/rxpm/src/test/kotlin/me/dmdev/rxpm/widget/InputControlTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2017-2021 Dmitriy Gorbunov (dmitriy.goto@gmail.com)
5 | * and Vasili Chyrvon (vasili.chyrvon@gmail.com)
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in all
15 | * copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | * SOFTWARE.
24 | */
25 |
26 | package me.dmdev.rxpm.widget
27 |
28 | import me.dmdev.rxpm.PresentationModel
29 | import me.dmdev.rxpm.PresentationModel.Lifecycle.CREATED
30 | import me.dmdev.rxpm.test.PmTestHelper
31 | import org.junit.Before
32 | import org.junit.Test
33 |
34 | class InputControlTest {
35 |
36 | private lateinit var presentationModel: PresentationModel
37 | private lateinit var pmTestHelper: PmTestHelper
38 |
39 | @Before
40 | fun setUp() {
41 | presentationModel = object : PresentationModel() {}
42 | pmTestHelper = PmTestHelper(presentationModel)
43 | }
44 |
45 | @Test fun formatInput() {
46 |
47 | val inputControl = presentationModel.inputControl(
48 | formatter = { it.toUpperCase() }
49 | )
50 | val testObserver = inputControl.text.observable.test()
51 |
52 | pmTestHelper.setLifecycleTo(CREATED)
53 |
54 | inputControl.textChanges.consumer.run {
55 | accept("a")
56 | accept("ab")
57 | accept("abc")
58 | }
59 |
60 | testObserver
61 | .assertValues(
62 | "", // initial value
63 | "A",
64 | "AB",
65 | "ABC"
66 | )
67 | .assertNoErrors()
68 | }
69 |
70 | @Test fun notFilterDuplicateValues() {
71 |
72 | val inputControl = presentationModel.inputControl(
73 | formatter = { it.take(3) }
74 | )
75 |
76 | val testObserver = inputControl.text.observable.test()
77 |
78 | pmTestHelper.setLifecycleTo(CREATED)
79 |
80 | inputControl.textChanges.consumer.run {
81 | accept("a")
82 | accept("ab")
83 | accept("abc")
84 | accept("abcd")
85 | }
86 |
87 | testObserver
88 | .assertValues(
89 | "", // initial value
90 | "a",
91 | "ab",
92 | "abc",
93 | "abc" // clear user input after formatting because editText contains "abcd"
94 | )
95 | .assertNoErrors()
96 | }
97 |
98 | @Test fun filterIfFocusNotChanged() {
99 |
100 | val inputControl = presentationModel.inputControl()
101 |
102 | val testObserver = inputControl.focus.observable.test()
103 |
104 | pmTestHelper.setLifecycleTo(CREATED)
105 |
106 | inputControl.focusChanges.consumer.run {
107 | accept(true)
108 | accept(true)
109 | accept(false)
110 | accept(false)
111 | accept(true)
112 | accept(true)
113 | }
114 |
115 | testObserver
116 | .assertValues(
117 | false, // initial value
118 | true,
119 | false,
120 | true
121 | )
122 | .assertNoErrors()
123 | }
124 | }
--------------------------------------------------------------------------------
/rxpm/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 |
7 | compileSdkVersion rootProject.compileSdkVersion
8 |
9 | defaultConfig {
10 | applicationId "me.dmdev.rxpm.sample"
11 | minSdkVersion 21
12 | targetSdkVersion rootProject.targetSdkVersion
13 | vectorDrawables.useSupportLibrary = true
14 | versionCode 1
15 | versionName "1.0"
16 | }
17 |
18 | compileOptions {
19 | sourceCompatibility JavaVersion.VERSION_1_8
20 | targetCompatibility JavaVersion.VERSION_1_8
21 | }
22 |
23 | sourceSets {
24 | main.java.srcDirs += 'src/main/kotlin'
25 | }
26 | }
27 |
28 | dependencies {
29 |
30 | implementation rootProject.kotlinStdlib
31 |
32 | implementation project(':rxpm')
33 |
34 | implementation rootProject.appCompat
35 | implementation rootProject.materialDesign
36 |
37 | // Rx
38 | implementation rootProject.rxAndroid2
39 | implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
40 |
41 | // RxBindings
42 | implementation rootProject.rxBinding
43 | implementation rootProject.rxBindingAppCompat
44 |
45 | implementation "com.jakewharton.timber:timber:4.7.1"
46 | implementation 'com.googlecode.libphonenumber:libphonenumber:8.11.1'
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/dmitriy/Develop/android-sdk-macosx/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/App.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample
2 |
3 | import android.app.Application
4 | import me.dmdev.rxpm.sample.main.MainComponent
5 | import timber.log.Timber
6 |
7 |
8 | class App : Application() {
9 |
10 | companion object {
11 | lateinit var component: MainComponent
12 | private set
13 | }
14 |
15 | override fun onCreate() {
16 | super.onCreate()
17 | component = MainComponent(this)
18 | initLogger()
19 | }
20 |
21 | private fun initLogger() {
22 | if (BuildConfig.DEBUG) {
23 | Timber.plant(Timber.DebugTree())
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/LaunchActivity.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample
2 |
3 | import android.app.*
4 | import android.content.*
5 | import android.os.*
6 | import androidx.appcompat.app.*
7 | import kotlinx.android.synthetic.main.activity_launch.*
8 | import me.dmdev.rxpm.sample.counter.*
9 | import me.dmdev.rxpm.sample.main.*
10 | import me.dmdev.rxpm.sample.validation.*
11 |
12 | class LaunchActivity : AppCompatActivity() {
13 |
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_launch)
17 |
18 | counterSample.setOnClickListener {
19 | launchActivity(CounterActivity::class.java)
20 | }
21 |
22 | mainSample.setOnClickListener {
23 | launchActivity(MainActivity::class.java)
24 | }
25 |
26 | formValidationSample.setOnClickListener {
27 | launchActivity(FormValidationActivity::class.java)
28 | }
29 | }
30 |
31 | private fun launchActivity(clazz: Class) {
32 | startActivity(Intent(this, clazz))
33 | }
34 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/counter/CounterActivity.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.counter
2 |
3 | import android.os.*
4 | import com.jakewharton.rxbinding3.view.*
5 | import kotlinx.android.synthetic.main.activity_counter.*
6 | import me.dmdev.rxpm.*
7 | import me.dmdev.rxpm.base.*
8 | import me.dmdev.rxpm.sample.R
9 |
10 | class CounterActivity : PmActivity() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | setContentView(R.layout.activity_counter)
15 | }
16 |
17 | override fun providePresentationModel() = CounterPm()
18 |
19 | override fun onBindPresentationModel(pm: CounterPm) {
20 |
21 | pm.count bindTo { counterText.text = it.toString() }
22 | pm.minusButtonEnabled bindTo minusButton::setEnabled
23 | pm.plusButtonEnabled bindTo plusButton::setEnabled
24 |
25 | minusButton.clicks() bindTo pm.minusButtonClicks
26 | plusButton.clicks() bindTo pm.plusButtonClicks
27 | }
28 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/counter/CounterPm.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.counter
2 |
3 | import me.dmdev.rxpm.*
4 |
5 | class CounterPm : PresentationModel() {
6 |
7 | companion object {
8 | const val MAX_COUNT = 10
9 | }
10 |
11 | val count = state(initialValue = 0)
12 |
13 | val minusButtonEnabled = state {
14 | count.observable.map { it > 0 }
15 | }
16 |
17 | val plusButtonEnabled = state {
18 | count.observable.map { it < MAX_COUNT }
19 | }
20 |
21 | val minusButtonClicks = action {
22 | this.filter { count.value > 0 }
23 | .map { count.value - 1 }
24 | .doOnNext(count.consumer)
25 | }
26 |
27 | val plusButtonClicks = action {
28 | this.filter { count.value < MAX_COUNT }
29 | .map { count.value + 1 }
30 | .doOnNext(count.consumer)
31 | }
32 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main
2 |
3 | import android.os.*
4 | import androidx.appcompat.app.*
5 | import me.dmdev.rxpm.navigation.*
6 | import me.dmdev.rxpm.sample.*
7 | import me.dmdev.rxpm.sample.main.AppNavigationMessage.*
8 | import me.dmdev.rxpm.sample.main.extensions.*
9 | import me.dmdev.rxpm.sample.main.ui.base.*
10 | import me.dmdev.rxpm.sample.main.ui.confirmation.*
11 | import me.dmdev.rxpm.sample.main.ui.country.*
12 | import me.dmdev.rxpm.sample.main.ui.main.*
13 | import me.dmdev.rxpm.sample.main.ui.phone.*
14 |
15 |
16 | class MainActivity : AppCompatActivity(), NavigationMessageHandler {
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | setContentView(R.layout.activity_main)
21 |
22 | if (savedInstanceState == null) {
23 | supportFragmentManager.openScreen(AuthByPhoneScreen(), addToBackStack = false)
24 | }
25 | }
26 |
27 | override fun onBackPressed() {
28 | supportFragmentManager.currentScreen?.let {
29 | if (it is BackHandler && it.handleBack()) return
30 | }
31 |
32 | if (supportFragmentManager.backStackEntryCount == 0) {
33 | super.onBackPressed()
34 | }
35 | }
36 |
37 | override fun handleNavigationMessage(message: NavigationMessage): Boolean {
38 |
39 | val sfm = supportFragmentManager
40 |
41 | when (message) {
42 |
43 | is Back -> super.onBackPressed()
44 |
45 | is ChooseCountry -> sfm.openScreen(ChooseCountryScreen())
46 |
47 | is CountryChosen -> {
48 | sfm.back()
49 | sfm.findScreen()?.onCountryChosen(message.country)
50 | }
51 |
52 | is PhoneSentSuccessfully -> sfm.openScreen(
53 | CodeConfirmationScreen.newInstance(message.phone)
54 | )
55 |
56 | is PhoneConfirmed -> {
57 | sfm.clearBackStack()
58 | sfm.openScreen(MainScreen(), addToBackStack = false)
59 | }
60 |
61 | is LogoutCompleted -> {
62 | sfm.clearBackStack()
63 | sfm.openScreen(AuthByPhoneScreen(), addToBackStack = false)
64 | }
65 | }
66 |
67 | return true
68 | }
69 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/MainComponent.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main
2 |
3 | import android.app.Application
4 | import me.dmdev.rxpm.sample.main.api.ServerApi
5 | import me.dmdev.rxpm.sample.main.api.ServerApiSimulator
6 | import me.dmdev.rxpm.sample.main.model.AuthModel
7 | import me.dmdev.rxpm.sample.main.model.TokenStorage
8 | import me.dmdev.rxpm.sample.main.util.PhoneUtil
9 | import me.dmdev.rxpm.sample.main.util.ResourceProvider
10 |
11 | class MainComponent(private val context: Application) {
12 |
13 | val resourceProvider by lazy { ResourceProvider(context) }
14 | val phoneUtil by lazy { PhoneUtil() }
15 |
16 | private val serverApi: ServerApi by lazy { ServerApiSimulator(context) }
17 | private val tokenStorage by lazy { TokenStorage() }
18 |
19 | val authModel by lazy { AuthModel(serverApi, tokenStorage) }
20 |
21 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/Messages.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main
2 |
3 | import me.dmdev.rxpm.navigation.*
4 | import me.dmdev.rxpm.sample.main.util.*
5 |
6 | sealed class AppNavigationMessage : NavigationMessage {
7 | object Back : AppNavigationMessage()
8 | object ChooseCountry : AppNavigationMessage()
9 | class CountryChosen(val country: Country) : AppNavigationMessage()
10 | class PhoneSentSuccessfully(val phone: String) : AppNavigationMessage()
11 | object PhoneConfirmed : AppNavigationMessage()
12 | object LogoutCompleted : AppNavigationMessage()
13 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/api/ServerApi.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.api
2 |
3 | import io.reactivex.Completable
4 | import io.reactivex.Single
5 |
6 |
7 | interface ServerApi {
8 | fun sendPhone(phone: String): Completable
9 | fun sendConfirmationCode(phone: String, code: String): Single
10 | fun logout(token: String): Completable
11 | }
12 |
13 | class WrongConfirmationCode(message: String) : Throwable(message)
14 | class ServerError(message: String) : Throwable(message)
15 |
16 | class TokenResponse(val token: String)
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/api/ServerApiSimulator.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.api
2 |
3 | import android.app.*
4 | import android.content.*
5 | import android.os.*
6 | import androidx.core.app.*
7 | import io.reactivex.*
8 | import me.dmdev.rxpm.sample.*
9 | import java.util.*
10 | import java.util.concurrent.*
11 |
12 |
13 | class ServerApiSimulator(private val context: Context) : ServerApi {
14 |
15 | companion object {
16 | private const val DELAY_IN_SECONDS = 3L
17 | private const val NOTIFICATION_ID = 112
18 | }
19 |
20 | private val notificationManager =
21 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
22 |
23 | private var phone: String? = null
24 | private var code: String? = null
25 | private val random = Random(System.currentTimeMillis())
26 |
27 | override fun sendPhone(phone: String): Completable {
28 |
29 | return Completable.complete()
30 | .delay(DELAY_IN_SECONDS, TimeUnit.SECONDS)
31 | .doOnComplete {
32 | maybeServerError()
33 | this.phone = phone
34 | code = generateRandomCode().toString()
35 | showNotification(code!!)
36 | }
37 | }
38 |
39 | override fun sendConfirmationCode(phone: String, code: String): Single {
40 |
41 | return Single.just(TokenResponse(UUID.randomUUID().toString()))
42 | .delay(DELAY_IN_SECONDS, TimeUnit.SECONDS)
43 | .doOnSuccess {
44 | if (this.code != code) {
45 | throw WrongConfirmationCode("Wrong confirmation code")
46 | } else {
47 | maybeServerError()
48 | }
49 | }
50 | .doOnSuccess {
51 | notificationManager.cancel(NOTIFICATION_ID)
52 | }
53 | }
54 |
55 | override fun logout(token: String): Completable {
56 |
57 | return Completable.complete()
58 | .delay(DELAY_IN_SECONDS, TimeUnit.SECONDS)
59 | .doOnComplete {
60 | maybeServerError()
61 | phone = null
62 | code = null
63 | }
64 | }
65 |
66 | private fun maybeServerError() {
67 | if (random.nextInt(100) >= 80) {
68 | throw ServerError("Service is unavailable. Please try again.")
69 | }
70 | }
71 |
72 | private fun generateRandomCode(): Int {
73 | var c = random.nextInt(10_000)
74 | if (c < 1000) {
75 | c = generateRandomCode()
76 | }
77 | return c
78 | }
79 |
80 | private fun showNotification(code: String) {
81 |
82 | val channelId = "rxpm_sample_channel"
83 |
84 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
85 | val channel = NotificationChannel(
86 | channelId,
87 | "RxPM sample channel",
88 | NotificationManager.IMPORTANCE_DEFAULT
89 | )
90 | notificationManager.createNotificationChannel(channel)
91 | }
92 |
93 | notificationManager
94 | .notify(
95 | NOTIFICATION_ID,
96 | NotificationCompat.Builder(context, channelId)
97 | .setContentTitle("RxPM Sample")
98 | .setContentText("Confirmation code $code")
99 | .setSmallIcon(R.mipmap.ic_launcher)
100 | .setDefaults(
101 | NotificationCompat.DEFAULT_SOUND
102 | or NotificationCompat.DEFAULT_LIGHTS
103 | )
104 | .build()
105 | )
106 | }
107 | }
108 |
109 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/extensions/FragmentManagerExtensions.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.extensions
2 |
3 | import androidx.fragment.app.*
4 | import me.dmdev.rxpm.sample.*
5 |
6 |
7 | fun FragmentManager.openScreen(
8 | fragment: Fragment,
9 | tag: String = fragment.javaClass.name,
10 | addToBackStack: Boolean = true
11 | ) {
12 | beginTransaction()
13 | .replace(R.id.container, fragment, tag)
14 | .also { if (addToBackStack) it.addToBackStack(null) }
15 | .commit()
16 | }
17 |
18 | inline val FragmentManager.currentScreen: Fragment?
19 | get() = this.findFragmentById(R.id.container)
20 |
21 | fun FragmentManager.back() {
22 | popBackStackImmediate()
23 | }
24 |
25 | inline fun FragmentManager.findScreen(): T? {
26 | return findFragmentByTag(T::class.java.name) as? T
27 | }
28 |
29 | fun FragmentManager.clearBackStack() {
30 | for (i in 0..backStackEntryCount) {
31 | popBackStackImmediate()
32 | }
33 | }
34 |
35 | fun FragmentManager.showDialog(
36 | dialog: DialogFragment,
37 | tag: String = dialog.javaClass.name
38 | ) {
39 | executePendingTransactions()
40 | findScreen()?.dismiss()
41 | dialog.show(this, tag)
42 | executePendingTransactions()
43 | }
44 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/extensions/UiExtensions.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.extensions
2 |
3 | import android.content.*
4 | import android.view.*
5 | import android.view.inputmethod.*
6 | import androidx.annotation.*
7 |
8 |
9 | fun View.visible(visible: Boolean) {
10 | this.visibility = if (visible) View.VISIBLE else View.GONE
11 | }
12 |
13 | fun ViewGroup.inflate(@LayoutRes layoutId: Int): View {
14 | return LayoutInflater.from(this.context).inflate(layoutId, this, false)
15 | }
16 |
17 | fun View.showKeyboard() {
18 | val function = {
19 | if (requestFocus()) {
20 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
21 | imm.showSoftInput(this, 0)
22 | }
23 | }
24 |
25 | function.invoke()
26 | post {
27 | function.invoke()
28 | }
29 | }
30 |
31 | fun View.hideKeyboard() {
32 | val function = {
33 | clearFocus()
34 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
35 | imm.hideSoftInputFromWindow(windowToken, 0)
36 | }
37 |
38 | function.invoke()
39 | post {
40 | function.invoke()
41 | }
42 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/model/AuthModel.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.model
2 |
3 | import io.reactivex.Completable
4 | import io.reactivex.schedulers.Schedulers
5 | import me.dmdev.rxpm.sample.main.api.ServerApi
6 | import me.dmdev.rxpm.sample.main.util.onlyDigits
7 |
8 |
9 | class AuthModel(
10 | private val api: ServerApi,
11 | private val tokenStorage: TokenStorage
12 | ) {
13 |
14 | fun isAuth() = tokenStorage.getToken().isNotEmpty()
15 |
16 | fun sendPhone(phone: String): Completable {
17 | return api.sendPhone(phone.onlyDigits())
18 | .subscribeOn(Schedulers.io())
19 | }
20 |
21 | fun sendConfirmationCode(phone: String, code: String): Completable {
22 | return api.sendConfirmationCode(phone.onlyDigits(), code.onlyDigits())
23 | .subscribeOn(Schedulers.io())
24 | .doOnSuccess { tokenStorage.saveToken(it.token) }
25 | .ignoreElement()
26 | }
27 |
28 | fun logout(): Completable {
29 | return api.logout(tokenStorage.getToken())
30 | .subscribeOn(Schedulers.io())
31 | .doOnComplete { tokenStorage.clear() }
32 | }
33 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/model/TokenStorage.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.model
2 |
3 | import java.util.concurrent.atomic.AtomicReference
4 |
5 |
6 | class TokenStorage {
7 |
8 | private var tokenRef = AtomicReference("")
9 |
10 | fun saveToken(token: String) {
11 | tokenRef.set(token)
12 | }
13 |
14 | fun getToken(): String {
15 | return tokenRef.get()
16 | }
17 |
18 | fun clear() {
19 | tokenRef.set("")
20 | }
21 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/BackHandler.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.base
2 |
3 | interface BackHandler {
4 | fun handleBack(): Boolean
5 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/ProgressDialog.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.base
2 |
3 | import android.app.*
4 | import android.os.*
5 | import android.widget.*
6 | import androidx.fragment.app.DialogFragment
7 | import me.dmdev.rxpm.sample.*
8 |
9 |
10 | class ProgressDialog : DialogFragment() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | isCancelable = false
15 | }
16 |
17 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
18 | return Dialog(requireContext(), R.style.ProgressDialogTheme).apply {
19 | setContentView(ProgressBar(context))
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/Screen.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.base
2 |
3 | import android.os.*
4 | import android.view.*
5 | import androidx.appcompat.app.*
6 | import io.reactivex.functions.*
7 | import me.dmdev.rxpm.*
8 | import me.dmdev.rxpm.base.*
9 | import me.dmdev.rxpm.sample.R
10 | import me.dmdev.rxpm.sample.main.extensions.*
11 | import me.dmdev.rxpm.widget.*
12 |
13 |
14 | abstract class Screen : PmFragment(), BackHandler {
15 |
16 | abstract val screenLayout: Int
17 |
18 | final override fun onCreateView(
19 | inflater: LayoutInflater,
20 | container: ViewGroup?,
21 | savedInstanceState: Bundle?
22 | ): View? {
23 | return inflater.inflate(screenLayout, container, false)
24 | }
25 |
26 | override fun onBindPresentationModel(pm: PM) {
27 | pm.errorDialog bindTo { message, _ ->
28 | AlertDialog.Builder(context!!)
29 | .setMessage(message)
30 | .setPositiveButton(R.string.ok_button, null)
31 | .create()
32 | }
33 | }
34 |
35 | override fun handleBack(): Boolean {
36 | Unit passTo presentationModel.backAction
37 | return true
38 | }
39 |
40 | val progressConsumer = Consumer {
41 | if (it) {
42 | childFragmentManager.showDialog(ProgressDialog())
43 | } else {
44 | childFragmentManager
45 | .findScreen()
46 | ?.dismiss()
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/base/ScreenPresentationModel.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.base
2 |
3 | import io.reactivex.functions.*
4 | import me.dmdev.rxpm.*
5 | import me.dmdev.rxpm.navigation.*
6 | import me.dmdev.rxpm.sample.main.AppNavigationMessage.*
7 | import me.dmdev.rxpm.widget.*
8 |
9 |
10 | abstract class ScreenPresentationModel : PresentationModel(), NavigationalPm {
11 |
12 | override val navigationMessages = command()
13 |
14 | val errorDialog = dialogControl()
15 |
16 | protected val errorConsumer = Consumer {
17 | errorDialog.show(it?.message ?: "Unknown error")
18 | }
19 |
20 | open val backAction = action {
21 | this.map { Back }
22 | .doOnNext(navigationMessages.consumer)
23 | }
24 |
25 | protected fun sendMessage(message: NavigationMessage) {
26 | navigationMessages.accept(message)
27 | }
28 |
29 | protected fun showError(errorMessage: String?) {
30 | errorDialog.show(errorMessage ?: "Unknown error")
31 | }
32 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationPm.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.confirmation
2 |
3 | import me.dmdev.rxpm.action
4 | import me.dmdev.rxpm.bindProgress
5 | import me.dmdev.rxpm.sample.R
6 | import me.dmdev.rxpm.sample.main.AppNavigationMessage.PhoneConfirmed
7 | import me.dmdev.rxpm.sample.main.model.AuthModel
8 | import me.dmdev.rxpm.sample.main.ui.base.ScreenPresentationModel
9 | import me.dmdev.rxpm.sample.main.util.ResourceProvider
10 | import me.dmdev.rxpm.sample.main.util.onlyDigits
11 | import me.dmdev.rxpm.skipWhileInProgress
12 | import me.dmdev.rxpm.state
13 | import me.dmdev.rxpm.validation.empty
14 | import me.dmdev.rxpm.validation.formValidator
15 | import me.dmdev.rxpm.validation.input
16 | import me.dmdev.rxpm.validation.minSymbols
17 | import me.dmdev.rxpm.widget.inputControl
18 |
19 | class CodeConfirmationPm(
20 | private val phone: String,
21 | private val resourceProvider: ResourceProvider,
22 | private val authModel: AuthModel
23 | ) : ScreenPresentationModel() {
24 |
25 | companion object {
26 | private const val CODE_LENGTH = 4
27 | }
28 |
29 | val code = inputControl(
30 | formatter = { it.onlyDigits().take(CODE_LENGTH) }
31 | )
32 | val inProgress = state(false)
33 |
34 | val sendButtonEnabled = state(false) {
35 | code.text.observable.map { it.length == CODE_LENGTH }
36 | }
37 |
38 | private val codeFilled = code.textChanges.observable
39 | .filter { it.length == CODE_LENGTH }
40 | .distinctUntilChanged()
41 | .map { Unit }
42 |
43 | val sendClicks = action {
44 | this.mergeWith(codeFilled)
45 | .skipWhileInProgress(inProgress)
46 | .map { code.text.value }
47 | .filter { formValidator.validate() }
48 | .switchMapCompletable { code ->
49 | authModel.sendConfirmationCode(phone, code)
50 | .bindProgress(inProgress)
51 | .doOnComplete { sendMessage(PhoneConfirmed) }
52 | .doOnError(errorConsumer)
53 | }
54 | .toObservable()
55 | }
56 |
57 | private val formValidator = formValidator {
58 | input(code) {
59 | empty(resourceProvider.getString(R.string.enter_confirmation_code))
60 | minSymbols(CODE_LENGTH, resourceProvider.getString(R.string.invalid_confirmation_code))
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/confirmation/CodeConfirmationScreen.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.confirmation
2 |
3 | import android.os.*
4 | import android.view.inputmethod.*
5 | import com.jakewharton.rxbinding3.appcompat.*
6 | import com.jakewharton.rxbinding3.view.*
7 | import com.jakewharton.rxbinding3.widget.*
8 | import io.reactivex.*
9 | import kotlinx.android.synthetic.main.screen_code_confirmation.*
10 | import me.dmdev.rxpm.*
11 | import me.dmdev.rxpm.sample.*
12 | import me.dmdev.rxpm.sample.R
13 | import me.dmdev.rxpm.sample.main.extensions.*
14 | import me.dmdev.rxpm.sample.main.ui.base.*
15 | import me.dmdev.rxpm.widget.*
16 |
17 |
18 | class CodeConfirmationScreen : Screen() {
19 |
20 | companion object {
21 | private const val ARG_PHONE = "arg_phone"
22 | fun newInstance(phone: String) = CodeConfirmationScreen().apply {
23 | arguments = Bundle().apply {
24 | putString(ARG_PHONE, phone)
25 | }
26 | }
27 | }
28 |
29 | override val screenLayout = R.layout.screen_code_confirmation
30 |
31 | override fun providePresentationModel(): CodeConfirmationPm {
32 | return CodeConfirmationPm(
33 | arguments!!.getString(ARG_PHONE)!!,
34 | App.component.resourceProvider,
35 | App.component.authModel
36 | )
37 | }
38 |
39 | override fun onBindPresentationModel(pm: CodeConfirmationPm) {
40 | super.onBindPresentationModel(pm)
41 |
42 | pm.code bindTo codeEditLayout
43 | pm.inProgress bindTo progressConsumer
44 | pm.sendButtonEnabled bindTo sendButton::setEnabled
45 |
46 | toolbar.navigationClicks() bindTo pm.backAction
47 |
48 | Observable
49 | .merge(
50 | sendButton.clicks(),
51 | codeEdit.editorActions()
52 | .filter { it == EditorInfo.IME_ACTION_SEND }
53 | .map { Unit }
54 | )
55 | .bindTo(pm.sendClicks)
56 |
57 | }
58 |
59 | override fun onResume() {
60 | super.onResume()
61 | codeEdit.showKeyboard()
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/ChooseCountryPm.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.country
2 |
3 | import me.dmdev.rxpm.*
4 | import me.dmdev.rxpm.sample.main.AppNavigationMessage.*
5 | import me.dmdev.rxpm.sample.main.ui.base.*
6 | import me.dmdev.rxpm.sample.main.ui.country.ChooseCountryPm.Mode.*
7 | import me.dmdev.rxpm.sample.main.util.*
8 | import me.dmdev.rxpm.widget.*
9 | import java.util.concurrent.*
10 |
11 |
12 | class ChooseCountryPm(private val phoneUtil: PhoneUtil) : ScreenPresentationModel() {
13 |
14 | enum class Mode { SEARCH_OPENED, SEARCH_CLOSED }
15 |
16 | val searchQueryInput = inputControl()
17 | val mode = state(SEARCH_CLOSED)
18 |
19 | val countries = state> {
20 | searchQueryInput.text.observable
21 | .debounce(100, TimeUnit.MILLISECONDS)
22 | .map { query ->
23 | val regex = "${query.toLowerCase()}.*".toRegex()
24 | phoneUtil.countries()
25 | .filter { it.name.toLowerCase().matches(regex) }
26 | .sortedWith(Comparator { c1, c2 ->
27 | compareValues(c1.name.toLowerCase(), c2.name.toLowerCase())
28 | })
29 | }
30 | }
31 |
32 | override val backAction = action {
33 | this.doOnNext {
34 | if (mode.value == SEARCH_OPENED) {
35 | mode.accept(SEARCH_CLOSED)
36 | } else {
37 | super.backAction.accept(Unit)
38 | }
39 | }
40 | }
41 |
42 | val clearClicks = action {
43 | this.doOnNext {
44 | if (searchQueryInput.text.value.isEmpty()) {
45 | mode.accept(SEARCH_CLOSED)
46 | } else {
47 | searchQueryInput.text.accept("")
48 | }
49 | }
50 | }
51 |
52 | val openSearchClicks = action {
53 | this.map { SEARCH_OPENED }
54 | .doOnNext(mode.consumer)
55 | }
56 |
57 | val countryClicks = action {
58 | this.doOnNext {
59 | sendMessage(CountryChosen(it))
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/ChooseCountryScreen.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.country
2 |
3 | import android.os.*
4 | import android.view.*
5 | import androidx.recyclerview.widget.*
6 | import com.jakewharton.rxbinding3.view.*
7 | import kotlinx.android.synthetic.main.screen_choose_country.*
8 | import me.dmdev.rxpm.*
9 | import me.dmdev.rxpm.sample.*
10 | import me.dmdev.rxpm.sample.R
11 | import me.dmdev.rxpm.sample.main.extensions.*
12 | import me.dmdev.rxpm.sample.main.ui.base.*
13 | import me.dmdev.rxpm.sample.main.ui.country.ChooseCountryPm.*
14 | import me.dmdev.rxpm.widget.*
15 |
16 |
17 | class ChooseCountryScreen : Screen() {
18 |
19 | private val countriesAdapter = CountriesAdapter(null) { country ->
20 | country passTo presentationModel.countryClicks
21 | }
22 |
23 | override val screenLayout = R.layout.screen_choose_country
24 |
25 | override fun providePresentationModel() = ChooseCountryPm(App.component.phoneUtil)
26 |
27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
28 | super.onViewCreated(view, savedInstanceState)
29 | with(recyclerView) {
30 | layoutManager = LinearLayoutManager(context)
31 | setHasFixedSize(true)
32 | adapter = countriesAdapter
33 | }
34 | }
35 |
36 | override fun onBindPresentationModel(pm: ChooseCountryPm) {
37 | super.onBindPresentationModel(pm)
38 |
39 | pm.mode bindTo {
40 | if (it == Mode.SEARCH_OPENED) {
41 | toolbarTitle.visible(false)
42 | searchQueryEdit.visible(true)
43 | searchQueryEdit.showKeyboard()
44 | searchButton.visible(false)
45 | clearButton.visible(true)
46 | } else {
47 | toolbarTitle.visible(true)
48 | searchQueryEdit.visible(false)
49 | searchQueryEdit.hideKeyboard()
50 | searchButton.visible(true)
51 | clearButton.visible(false)
52 | }
53 | }
54 |
55 | pm.searchQueryInput bindTo searchQueryEdit
56 | pm.countries bindTo { countriesAdapter.setData(it) }
57 |
58 | searchButton.clicks() bindTo pm.openSearchClicks
59 | clearButton.clicks() bindTo pm.clearClicks
60 | navButton.clicks() bindTo pm.backAction
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/country/CountriesAdapter.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.country
2 |
3 | import android.annotation.*
4 | import android.view.*
5 | import androidx.recyclerview.widget.*
6 | import kotlinx.android.synthetic.main.item_country.view.*
7 | import me.dmdev.rxpm.sample.*
8 | import me.dmdev.rxpm.sample.main.extensions.*
9 | import me.dmdev.rxpm.sample.main.util.*
10 |
11 |
12 | class CountriesAdapter(
13 | private var countries: List?,
14 | private val itemClickListener: (country: Country) -> Unit
15 | ) : RecyclerView.Adapter() {
16 |
17 | fun setData(countries: List) {
18 | this.countries = countries
19 | notifyDataSetChanged()
20 | }
21 |
22 | override fun getItemCount() = countries?.size ?: 0
23 |
24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
25 | return ViewHolder(parent.inflate(R.layout.item_country))
26 | }
27 |
28 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
29 | holder.bind(countries!![position])
30 | }
31 |
32 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
33 |
34 | private lateinit var country: Country
35 |
36 | init {
37 | itemView.setOnClickListener {
38 | itemClickListener.invoke(country)
39 | }
40 | }
41 |
42 | @SuppressLint("SetTextI18n")
43 | fun bind(country: Country) {
44 | this.country = country
45 | itemView.countryName.text = country.name
46 | itemView.countryCode.text = "+${country.countryCallingCode}"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/main/MainPm.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.main
2 |
3 | import me.dmdev.rxpm.*
4 | import me.dmdev.rxpm.sample.main.AppNavigationMessage.*
5 | import me.dmdev.rxpm.sample.main.model.*
6 | import me.dmdev.rxpm.sample.main.ui.base.*
7 | import me.dmdev.rxpm.widget.*
8 |
9 | class MainPm(private val authModel: AuthModel) : ScreenPresentationModel() {
10 |
11 | sealed class DialogResult {
12 | object Ok : DialogResult()
13 | object Cancel : DialogResult()
14 | }
15 |
16 | val logoutDialog = dialogControl()
17 | val inProgress = state(false)
18 |
19 | val logoutClicks = action {
20 | this.skipWhileInProgress(inProgress)
21 | .switchMapMaybe {
22 | logoutDialog.showForResult(Unit)
23 | .filter { it == DialogResult.Ok }
24 | }
25 | .switchMapCompletable {
26 | authModel.logout()
27 | .bindProgress(inProgress)
28 | .doOnError { showError(it.message) }
29 | .doOnComplete { sendMessage(LogoutCompleted) }
30 | }
31 | .toObservable()
32 | }
33 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/main/MainScreen.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.main
2 |
3 | import android.os.*
4 | import android.view.*
5 | import androidx.appcompat.app.*
6 | import com.jakewharton.rxbinding3.appcompat.*
7 | import kotlinx.android.synthetic.main.screen_main.*
8 | import me.dmdev.rxpm.*
9 | import me.dmdev.rxpm.sample.*
10 | import me.dmdev.rxpm.sample.R
11 | import me.dmdev.rxpm.sample.main.ui.base.*
12 | import me.dmdev.rxpm.sample.main.ui.main.MainPm.DialogResult.*
13 | import me.dmdev.rxpm.widget.*
14 |
15 |
16 | class MainScreen : Screen() {
17 |
18 | override val screenLayout = R.layout.screen_main
19 |
20 | override fun providePresentationModel() = MainPm(App.component.authModel)
21 |
22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
23 | super.onViewCreated(view, savedInstanceState)
24 | toolbar.inflateMenu(R.menu.main)
25 | }
26 |
27 | override fun onBindPresentationModel(pm: MainPm) {
28 | super.onBindPresentationModel(pm)
29 |
30 | pm.logoutDialog bindTo { _, dc ->
31 | AlertDialog.Builder(context!!)
32 | .setMessage("Are you sure you want to log out?")
33 | .setPositiveButton("ok") { _, _ -> dc.sendResult(Ok) }
34 | .setNegativeButton("cancel") { _, _ -> dc.sendResult(Cancel) }
35 | .create()
36 | }
37 |
38 | pm.inProgress bindTo progressConsumer
39 |
40 | toolbar.itemClicks()
41 | .filter { it.itemId == R.id.logoutAction }
42 | .map { Unit }
43 | .bindTo(pm.logoutClicks)
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/phone/AuthByPhonePm.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.phone
2 |
3 | import com.google.i18n.phonenumbers.*
4 | import io.reactivex.rxkotlin.Observables.combineLatest
5 | import me.dmdev.rxpm.*
6 | import me.dmdev.rxpm.sample.R
7 | import me.dmdev.rxpm.sample.main.*
8 | import me.dmdev.rxpm.sample.main.AppNavigationMessage.*
9 | import me.dmdev.rxpm.sample.main.model.*
10 | import me.dmdev.rxpm.sample.main.ui.base.*
11 | import me.dmdev.rxpm.sample.main.util.*
12 | import me.dmdev.rxpm.validation.*
13 | import me.dmdev.rxpm.widget.*
14 |
15 |
16 | class AuthByPhonePm(
17 | private val phoneUtil: PhoneUtil,
18 | private val resourceProvider: ResourceProvider,
19 | private val authModel: AuthModel
20 | ) : ScreenPresentationModel() {
21 |
22 | val phoneNumber = inputControl(formatter = null)
23 | val countryCode = inputControl(
24 | initialText = "+7",
25 | formatter = {
26 | val code = "+${it.onlyDigits().take(5)}"
27 | if (code.length > 5) {
28 | try {
29 | val number = phoneUtil.parsePhone(code)
30 | phoneNumber.focus.accept(true)
31 | phoneNumber.textChanges.accept(number.nationalNumber.toString())
32 | "+${number.countryCode}"
33 | } catch (e: NumberParseException) {
34 | code
35 | }
36 | } else {
37 | code
38 | }
39 | }
40 | )
41 | val chosenCountry = state {
42 | countryCode.text.observable
43 | .map {
44 | val code = it.onlyDigits()
45 | if (code.isNotEmpty()) {
46 | phoneUtil.getCountryForCountryCode(code.onlyDigits().toInt())
47 | } else {
48 | Country.UNKNOWN
49 | }
50 | }
51 | }
52 |
53 | val inProgress = state(false)
54 |
55 | val sendButtonEnabled = state(false) {
56 | combineLatest(
57 | phoneNumber.textChanges.observable,
58 | chosenCountry.observable
59 | ) { number: String, country: Country ->
60 | phoneUtil.isValidPhone(country, number)
61 | }
62 | }
63 |
64 | val countryClicks = action {
65 | this.map { AppNavigationMessage.ChooseCountry }
66 | .doOnNext(navigationMessages.consumer)
67 | }
68 |
69 | val chooseCountry = action {
70 | this.doOnNext {
71 | countryCode.textChanges.accept("+${it.countryCallingCode}")
72 | chosenCountry.accept(it)
73 | phoneNumber.focus.accept(true)
74 | }
75 | }
76 |
77 | val sendClicks = action {
78 | this.skipWhileInProgress(inProgress)
79 | .filter { formValidator.validate() }
80 | .map { "${countryCode.text.value} ${phoneNumber.text.value}" }
81 | .switchMapCompletable { phone ->
82 | authModel.sendPhone(phone)
83 | .bindProgress(inProgress)
84 | .doOnComplete { sendMessage(PhoneSentSuccessfully(phone)) }
85 | .doOnError(errorConsumer)
86 | }
87 | .toObservable()
88 | }
89 |
90 | override fun onCreate() {
91 | super.onCreate()
92 |
93 | combineLatest(
94 | phoneNumber.textChanges.observable,
95 | chosenCountry.observable
96 | ) { number: String, country: Country ->
97 | phoneUtil.formatPhoneNumber(country, number)
98 | }
99 | .subscribe(phoneNumber.text)
100 | .untilDestroy()
101 | }
102 |
103 | private val formValidator = formValidator {
104 | input(phoneNumber) {
105 | empty(resourceProvider.getString(R.string.enter_phone_number))
106 | valid(
107 | validation = { phoneNumber ->
108 | phoneUtil.isValidPhone(chosenCountry.value, phoneNumber)
109 | },
110 | errorMessage = resourceProvider.getString(R.string.invalid_phone_number)
111 | )
112 | }
113 | }
114 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/ui/phone/AuthByPhoneScreen.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.ui.phone
2 |
3 | import android.view.inputmethod.*
4 | import com.jakewharton.rxbinding3.view.*
5 | import com.jakewharton.rxbinding3.widget.*
6 | import io.reactivex.*
7 | import kotlinx.android.synthetic.main.screen_auth_by_phone.*
8 | import me.dmdev.rxpm.*
9 | import me.dmdev.rxpm.sample.*
10 | import me.dmdev.rxpm.sample.R
11 | import me.dmdev.rxpm.sample.main.extensions.*
12 | import me.dmdev.rxpm.sample.main.ui.base.*
13 | import me.dmdev.rxpm.sample.main.util.*
14 | import me.dmdev.rxpm.widget.*
15 |
16 |
17 | class AuthByPhoneScreen : Screen() {
18 |
19 | override val screenLayout = R.layout.screen_auth_by_phone
20 |
21 | override fun providePresentationModel(): AuthByPhonePm {
22 | return AuthByPhonePm(
23 | App.component.phoneUtil,
24 | App.component.resourceProvider,
25 | App.component.authModel
26 | )
27 | }
28 |
29 | override fun onBindPresentationModel(pm: AuthByPhonePm) {
30 | super.onBindPresentationModel(pm)
31 |
32 | pm.countryCode bindTo editCountryCodeLayout
33 | pm.phoneNumber bindTo editPhoneNumberLayout
34 | pm.chosenCountry bindTo { countryName.text = it.name }
35 |
36 | pm.inProgress bindTo progressConsumer
37 | pm.sendButtonEnabled bindTo sendButton::setEnabled
38 |
39 | countryName.clicks() bindTo pm.countryClicks
40 |
41 | Observable
42 | .merge(
43 | sendButton.clicks(),
44 | phoneNumberEdit.editorActions()
45 | .filter { it == EditorInfo.IME_ACTION_SEND }
46 | .map { Unit }
47 | )
48 | .bindTo(pm.sendClicks)
49 |
50 | }
51 |
52 | fun onCountryChosen(country: Country) {
53 | country passTo presentationModel.chooseCountry
54 | }
55 |
56 | override fun onResume() {
57 | super.onResume()
58 | phoneNumberEdit.showKeyboard()
59 | }
60 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/util/Country.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.util
2 |
3 | import java.util.*
4 |
5 |
6 | class Country(val region: String, val countryCallingCode: Int) {
7 |
8 | companion object {
9 | private const val UNKNOWN_REGION = "ZZ"
10 | private const val INVALID_COUNTRY_CODE = 0
11 | val UNKNOWN = Country(UNKNOWN_REGION, INVALID_COUNTRY_CODE)
12 | }
13 |
14 | val name = Locale("en", region).getDisplayCountry(Locale.ENGLISH)!!
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/util/PhoneUtil.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.util
2 |
3 | import com.google.i18n.phonenumbers.*
4 | import java.util.*
5 |
6 | private const val MAX_PHONE_LENGTH = 17
7 |
8 | fun onlyPhone(phoneNumberString: String): String {
9 | return "+${phoneNumberString.onlyDigits()}"
10 | }
11 |
12 | fun String.onlyDigits() = this.replace("\\D".toRegex(), "")
13 |
14 | class PhoneUtil {
15 |
16 | private val countriesMap = HashMap()
17 | private val phoneNumberUtil = PhoneNumberUtil.getInstance()
18 |
19 | init {
20 | for (region in phoneNumberUtil.supportedRegions) {
21 | val country = Country(region, phoneNumberUtil.getCountryCodeForRegion(region))
22 | countriesMap[region] = country
23 | }
24 | }
25 |
26 | @Throws(NumberParseException::class)
27 | fun parsePhone(phone: String): Phonenumber.PhoneNumber {
28 | return phoneNumberUtil.parse(phone, PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY)
29 | }
30 |
31 | fun formatPhoneNumber(country: Country, phoneNumber: String): String {
32 |
33 | if (country === Country.UNKNOWN) return phoneNumber.onlyDigits()
34 |
35 | val code = "+${country.countryCallingCode}"
36 | var formattedPhone = code + phoneNumber.onlyDigits()
37 |
38 | val asYouTypeFormatter = phoneNumberUtil.getAsYouTypeFormatter(country.region)
39 |
40 | for (ch in (code + phoneNumber.onlyDigits()).toCharArray()) {
41 | formattedPhone = asYouTypeFormatter.inputDigit(ch)
42 | }
43 |
44 | return formattedPhone.replace(code, "").trim()
45 | }
46 |
47 | fun formatPhoneNumber(phoneNumberString: String): String {
48 |
49 | val phoneNumber = onlyPhone(phoneNumberString).take(MAX_PHONE_LENGTH)
50 | var formattedPhone: String = phoneNumber
51 |
52 | val asYouTypeFormatter =
53 | phoneNumberUtil.getAsYouTypeFormatter(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY)
54 |
55 | for (ch in phoneNumber.toCharArray()) {
56 | formattedPhone = asYouTypeFormatter.inputDigit(ch)
57 | }
58 |
59 | return formattedPhone.trim()
60 | }
61 |
62 | fun isValidPhone(phoneNumber: String): Boolean {
63 | return try {
64 | phoneNumberUtil.isValidNumber(
65 | phoneNumberUtil.parse(onlyPhone(phoneNumber), null)
66 | )
67 | } catch (e: Exception) {
68 | false
69 | }
70 | }
71 |
72 | fun isValidPhone(country: Country, phoneNumber: String): Boolean {
73 | return try {
74 | val number = Phonenumber.PhoneNumber().apply {
75 | countryCode = country.countryCallingCode
76 | nationalNumber = phoneNumber.onlyDigits().toLong()
77 | }
78 | phoneNumberUtil.isValidNumberForRegion(number, country.region)
79 | } catch (e: NumberFormatException) {
80 | false
81 | }
82 | }
83 |
84 | fun getCountryForCountryCode(code: Int): Country {
85 | return countriesMap[phoneNumberUtil.getRegionCodeForCountryCode(code)] ?: Country.UNKNOWN
86 | }
87 |
88 | fun countries(): List {
89 | return countriesMap.values.toList()
90 | }
91 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/main/util/ResourcesProvider.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.main.util
2 |
3 | import android.content.*
4 | import androidx.annotation.*
5 |
6 |
7 | class ResourceProvider(private val context: Context) {
8 |
9 | fun getString(@StringRes resId: Int, vararg formatArgs: Any): String {
10 | return context.resources.getString(resId, *formatArgs)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/validation/FormValidationActivity.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.validation
2 |
3 | import android.os.*
4 | import android.widget.*
5 | import com.jakewharton.rxbinding3.view.*
6 | import kotlinx.android.synthetic.main.activity_form.*
7 | import me.dmdev.rxpm.*
8 | import me.dmdev.rxpm.base.*
9 | import me.dmdev.rxpm.sample.*
10 | import me.dmdev.rxpm.sample.R
11 | import me.dmdev.rxpm.widget.*
12 |
13 | class FormValidationActivity : PmActivity() {
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | setContentView(R.layout.activity_form)
18 | }
19 |
20 | override fun providePresentationModel(): FormValidationPm {
21 | return FormValidationPm(App.component.phoneUtil)
22 | }
23 |
24 | override fun onBindPresentationModel(pm: FormValidationPm) {
25 | pm.name bindTo nameEditLayout
26 | pm.email bindTo emailEditLayout
27 | pm.phone bindTo phoneEditLayout
28 | pm.password bindTo passwordEditLayout
29 | pm.confirmPassword bindTo confirmPasswordEditLayout
30 | pm.termsCheckBox bindTo termsCheckbox
31 |
32 | pm.acceptTermsOfUse bindTo {
33 | Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
34 | }
35 |
36 | validateButton.clicks() bindTo pm.validateButtonClicks
37 | }
38 | }
--------------------------------------------------------------------------------
/sample/src/main/kotlin/me/dmdev/rxpm/sample/validation/FormValidationPm.kt:
--------------------------------------------------------------------------------
1 | package me.dmdev.rxpm.sample.validation
2 |
3 | import me.dmdev.rxpm.*
4 | import me.dmdev.rxpm.sample.main.util.*
5 | import me.dmdev.rxpm.validation.*
6 | import me.dmdev.rxpm.widget.*
7 |
8 | class FormValidationPm(
9 | private val phoneUtil: PhoneUtil
10 | ) : PresentationModel() {
11 |
12 | val name = inputControl(
13 | formatter = { it.replace("[^a-zA-Z ]".toRegex(), "").take(100) }
14 | )
15 | val email = inputControl()
16 | val phone = inputControl(
17 | initialText = "+7",
18 | formatter = { phoneUtil.formatPhoneNumber(it) }
19 | )
20 | val password = inputControl()
21 | val confirmPassword = inputControl()
22 |
23 | val termsCheckBox = checkControl(false)
24 |
25 | val acceptTermsOfUse = command()
26 |
27 | val validateButtonClicks = action {
28 | doOnNext { formValidator.validate() }
29 | }
30 |
31 | private val formValidator = formValidator {
32 |
33 | input(name) {
34 | empty("Input Name")
35 | }
36 |
37 | input(email, required = false) {
38 | pattern(ANDROID_EMAIL_PATTERN, "Invalid e-mail address")
39 | }
40 |
41 | input(phone, validateOnFocusLoss = true) {
42 | valid(phoneUtil::isValidPhone, "Invalid phone number")
43 | }
44 |
45 | input(password) {
46 | empty("Input Password")
47 | minSymbols(6, "Minimum 6 symbols")
48 | pattern(
49 | regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[\\d]).{6,}\$",
50 | errorMessage = "The password must contain a large and small letters, numbers."
51 | )
52 | }
53 |
54 | input(confirmPassword) {
55 | empty("Confirm Password")
56 | equalsTo(password, "Passwords do not match")
57 | }
58 |
59 | check(termsCheckBox) {
60 | acceptTermsOfUse.accept("Please accept the terms of use")
61 | }
62 | }
63 | }
64 |
65 | private const val ANDROID_EMAIL_PATTERN = "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
66 | "\\@" +
67 | "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
68 | "(" +
69 | "\\." +
70 | "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
71 | ")+"
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/bg_edit_country.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_add_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_arrow_back_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_close_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_exit_to_app_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_remove_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_search_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_counter.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
26 |
27 |
34 |
35 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_launch.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
25 |
26 |
34 |
35 |
43 |
44 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/item_country.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
22 |
32 |
33 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/layout_loading_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
20 |
21 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/screen_auth_by_phone.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
27 |
28 |
33 |
34 |
40 |
41 |
46 |
47 |
48 |
49 |
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
75 |
76 |
82 |
83 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/screen_choose_country.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
20 |
21 |
30 |
31 |
44 |
45 |
50 |
51 |
55 |
56 |
57 |
58 |
63 |
64 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/screen_code_confirmation.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
23 |
24 |
31 |
32 |
33 |
34 |
41 |
42 |
48 |
49 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/screen_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/sample/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmdevgo/RxPM/a7c5890553cf96b13e14055697b83d196c4f4100/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #607D8B
4 | #455A64
5 | #CFD8DC
6 | #FF9800
7 | #212121
8 | #757575
9 | #FFFFFF
10 | #BDBDBD
11 | #f4f4f4
12 |
13 | #000000
14 | #4D000000
15 | #80000000
16 | #00000000
17 | #FFFFFF
18 |
19 |
20 | #000000
21 | #DE000000
22 | #8A000000
23 | @color/secondary_text_dark
24 | #61000000
25 | @color/hint_text_dark
26 | #1F000000
27 |
28 | #FFFFFF
29 | #DEFFFFFF
30 | #8AFFFFFF
31 | @color/secondary_text_light
32 | #61FFFFFF
33 | @color/hint_text_light
34 | #1FFFFFFF
35 |
36 |
37 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | RxPM
4 | OK
5 |
6 |
7 | RxPM samples
8 | Main Sample
9 | Counter Sample
10 | Form Validation
11 |
12 |
13 | Please confirm your country code and enter your phone number.
14 | @string/app_name
15 | Enter phone number
16 | Invalid phone number
17 | Send phone
18 |
19 |
20 | Choose a country
21 | Search
22 | Send code
23 |
24 |
25 | Please enter confirmation code from the notification.
26 | @string/app_name
27 | Code
28 | Enter confirmation code
29 | Enter four digits
30 |
31 |
32 | Logout
33 |
34 |
35 | Form Validation
36 | Name
37 | Validate
38 | E-mail
39 | Phone
40 | Password
41 | Confirm Password
42 | Accept Terms of Use
43 |
44 |
45 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
22 |
23 |
28 |
29 |
32 |
33 |
36 |
37 |
42 |
43 |
46 |
47 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample', ':rxpm'
2 |
--------------------------------------------------------------------------------