├── .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 |