├── app ├── .gitignore ├── src │ ├── test │ │ ├── resources │ │ │ └── mockito-extensions │ │ │ │ └── org.mockito.plugins.MockMaker │ │ └── kotlin │ │ │ └── com │ │ │ └── cesarvaliente │ │ │ └── kunidirectional │ │ │ ├── RoboTestApplication.kt │ │ │ ├── TestStore.kt │ │ │ ├── TestUtils.kt │ │ │ ├── ExtensionsTest.kt │ │ │ ├── ControllerViewTest.kt │ │ │ ├── itemslist │ │ │ └── ItemsControllerViewTest.kt │ │ │ └── edititem │ │ │ └── EditItemControllerViewTest.kt │ └── main │ │ ├── res │ │ ├── main │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── styles.xml │ │ │ │ └── colors.xml │ │ │ └── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ ├── edititem │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ └── layout │ │ │ │ └── edit_item_layout.xml │ │ └── itemslist │ │ │ ├── drawable-hdpi │ │ │ ├── ic_add_black.png │ │ │ ├── ic_star_black.png │ │ │ └── ic_star_border_black.png │ │ │ ├── drawable-mdpi │ │ │ ├── ic_add_black.png │ │ │ ├── ic_star_black.png │ │ │ └── ic_star_border_black.png │ │ │ ├── drawable-xhdpi │ │ │ ├── ic_add_black.png │ │ │ ├── ic_star_black.png │ │ │ └── ic_star_border_black.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── ic_add_black.png │ │ │ ├── ic_star_black.png │ │ │ └── ic_star_border_black.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── ic_add_black.png │ │ │ ├── ic_star_black.png │ │ │ └── ic_star_border_black.png │ │ │ ├── values │ │ │ └── strings.xml │ │ │ ├── menu │ │ │ └── items_menu.xml │ │ │ └── layout │ │ │ ├── item_layout.xml │ │ │ └── items_layout.xml │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── com │ │ └── cesarvaliente │ │ └── kunidirectional │ │ ├── LifecycleCallbacks.kt │ │ ├── MainThread.kt │ │ ├── AppStore.kt │ │ ├── itemslist │ │ ├── recyclerview │ │ │ ├── ItemsDiffCallback.kt │ │ │ ├── ItemTouchHelperCallback.kt │ │ │ ├── ItemViewHolder.kt │ │ │ └── ItemsAdapter.kt │ │ ├── ItemsControllerView.kt │ │ └── ItemsActivity.kt │ │ ├── ItemsApplication.kt │ │ ├── ViewActivity.kt │ │ ├── ControllerView.kt │ │ ├── edititem │ │ ├── EditItemControllerView.kt │ │ └── EditItemActivity.kt │ │ └── Extensions.kt ├── proguard-rules.pro └── build.gradle ├── store ├── .gitignore ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── cesarvaliente │ │ └── kunidirectional │ │ └── store │ │ ├── IterableExtensions.kt │ │ ├── State.kt │ │ ├── Models.kt │ │ ├── Subscribers.kt │ │ ├── reducer │ │ ├── DeleteReducer.kt │ │ ├── CreationReducer.kt │ │ ├── ReadReducer.kt │ │ ├── NavigationReducer.kt │ │ ├── UpdateReducer.kt │ │ └── Reducer.kt │ │ ├── Actions.kt │ │ ├── Threading.kt │ │ └── Store.kt │ └── test │ └── kotlin │ └── com │ └── cesarvaliente │ └── kunidirectional │ ├── reducer │ ├── ReducerTestUtils.kt │ ├── ReadReducerTest.kt │ ├── NavigationReducerTest.kt │ ├── CreationReducerTest.kt │ ├── DeleteReducerTest.kt │ └── UpdateReducerTest.kt │ ├── PositionsFactoryTest.kt │ ├── ModelsTest.kt │ └── IterableExtensionsTest.kt ├── persistence ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── cesarvaliente │ │ │ └── kunidirectional │ │ │ └── persistence │ │ │ ├── handler │ │ │ ├── ActionHandler.kt │ │ │ ├── DeleteHandler.kt │ │ │ ├── ReadHandler.kt │ │ │ ├── CreationHandler.kt │ │ │ └── UpdateHandler.kt │ │ │ ├── Mapper.kt │ │ │ ├── PersistenceSideEffect.kt │ │ │ ├── PersistenceFunctions.kt │ │ │ └── Models.kt │ ├── test │ │ └── kotlin │ │ │ └── com │ │ │ └── cesarvaliente │ │ │ └── kunidirectional │ │ │ └── persistence │ │ │ ├── PersistenceSideEffectTest.kt │ │ │ ├── ModelsTest.kt │ │ │ └── MapperTest.kt │ └── androidTest │ │ └── kotlin │ │ └── com │ │ └── cesarvaliente │ │ └── kunidirectional │ │ └── persistence │ │ ├── TestUtils.kt │ │ ├── handler │ │ ├── DeleteHandlerTest.kt │ │ ├── CreationHandlerTest.kt │ │ ├── ReadHandlerTest.kt │ │ └── UpdateHandlerTest.kt │ │ └── PersistenceFunctionsTest.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── art ├── diagrams │ ├── architecture.png │ └── dependencies.png └── screenshots │ ├── edit_item.png │ ├── empty_list.png │ ├── items_list.png │ ├── item_deleted.png │ ├── item_starred_red.png │ ├── item_updated_red.png │ ├── persistence_menu.png │ ├── update_item_red.png │ └── item_creted_white.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── version.properties ├── gradle.properties ├── gradlew.bat ├── dependencies.gradle └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.iml 3 | -------------------------------------------------------------------------------- /store/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.iml 3 | -------------------------------------------------------------------------------- /persistence/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.iml 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':store', ':persistence' 2 | -------------------------------------------------------------------------------- /app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline 2 | -------------------------------------------------------------------------------- /art/diagrams/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/diagrams/architecture.png -------------------------------------------------------------------------------- /art/diagrams/dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/diagrams/dependencies.png -------------------------------------------------------------------------------- /art/screenshots/edit_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/screenshots/edit_item.png -------------------------------------------------------------------------------- /art/screenshots/empty_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/screenshots/empty_list.png -------------------------------------------------------------------------------- /art/screenshots/items_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/screenshots/items_list.png -------------------------------------------------------------------------------- /art/screenshots/item_deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/screenshots/item_deleted.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /art/screenshots/item_starred_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/screenshots/item_starred_red.png -------------------------------------------------------------------------------- /art/screenshots/item_updated_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/screenshots/item_updated_red.png -------------------------------------------------------------------------------- /art/screenshots/persistence_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/screenshots/persistence_menu.png -------------------------------------------------------------------------------- /art/screenshots/update_item_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/screenshots/update_item_red.png -------------------------------------------------------------------------------- /art/screenshots/item_creted_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/art/screenshots/item_creted_white.png -------------------------------------------------------------------------------- /app/src/main/res/main/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/main/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/main/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/main/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /persistence/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/main/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/main/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/main/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/main/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/main/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | KUnidirectional 4 | -------------------------------------------------------------------------------- /app/src/main/res/main/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/main/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/edititem/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Item text.. 4 | -------------------------------------------------------------------------------- /app/src/main/res/main/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/main/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/main/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/main/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/main/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/main/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-hdpi/ic_add_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-hdpi/ic_add_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-hdpi/ic_star_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-hdpi/ic_star_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-mdpi/ic_add_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-mdpi/ic_add_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-mdpi/ic_star_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-mdpi/ic_star_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-xhdpi/ic_add_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-xhdpi/ic_add_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-xhdpi/ic_star_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-xhdpi/ic_star_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-xxhdpi/ic_add_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-xxhdpi/ic_add_black.png -------------------------------------------------------------------------------- /app/src/main/res/main/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/main/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/main/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/main/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-xxhdpi/ic_star_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-xxhdpi/ic_star_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-xxxhdpi/ic_add_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-xxxhdpi/ic_add_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-xxxhdpi/ic_star_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-xxxhdpi/ic_star_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-hdpi/ic_star_border_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-hdpi/ic_star_border_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-mdpi/ic_star_border_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-mdpi/ic_star_border_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-xhdpi/ic_star_border_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-xhdpi/ic_star_border_black.png -------------------------------------------------------------------------------- /persistence/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | persistence 3 | kunidirectional.realm 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-xxhdpi/ic_star_border_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-xxhdpi/ic_star_border_black.png -------------------------------------------------------------------------------- /app/src/main/res/itemslist/drawable-xxxhdpi/ic_star_border_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesarValiente/KUnidirectional/HEAD/app/src/main/res/itemslist/drawable-xxxhdpi/ic_star_border_black.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 21 20:16:27 CET 2017 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-4.1-all.zip 7 | -------------------------------------------------------------------------------- /store/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | dependencies { 4 | compile kotlinDependencies.kotlinStdlib 5 | 6 | testCompile unitTestDependencies.junit 7 | testCompile unitTestDependencies.mockito 8 | testCompile unitTestDependencies.mockitoKotlin 9 | testCompile unitTestDependencies.hamcrestLibrary 10 | testCompile unitTestDependencies.hamcrestCore 11 | } -------------------------------------------------------------------------------- /app/src/main/res/itemslist/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Item deleted 3 | Undo 4 | 5 | prefPersistence 6 | Persistence 7 | You have to restart the app so the change is enabled 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/main/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/itemslist/menu/items_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/main/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | #FF5050 8 | #FFFF66 9 | #66FF66 10 | #66CCFF 11 | #FFFFFF 12 | 13 | @color/colorPrimary 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/ 39 | 40 | # Keystore files 41 | *.jks -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2014 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | code=1 17 | name=0.1 18 | applicationId=com.cesarvaliente.kunidirectional 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/test/kotlin/com/cesarvaliente/kunidirectional/RoboTestApplication.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import android.app.Application 23 | 24 | class RoboTestApplication : Application() 25 | 26 | -------------------------------------------------------------------------------- /app/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/Cesar/Library/Android/sdk/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 | -------------------------------------------------------------------------------- /persistence/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/Cesar/Library/Android/sdk/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 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/LifecycleCallbacks.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | interface LifecycleCallbacks { 23 | 24 | var isActivityRunning: Boolean 25 | 26 | fun onStart() 27 | fun onResume() 28 | fun onPause() 29 | fun onStop() 30 | fun onDestroy() 31 | } 32 | -------------------------------------------------------------------------------- /persistence/src/main/kotlin/com/cesarvaliente/kunidirectional/persistence/handler/ActionHandler.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence.handler 21 | 22 | import com.cesarvaliente.kunidirectional.store.Action 23 | 24 | internal interface ActionHandler { 25 | fun handle(action: T, actionDispatcher: (Action) -> Unit) 26 | } 27 | -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/IterableExtensions.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store 21 | 22 | inline fun Iterable.firstOrDefault(predicate: (T) -> Boolean?, default: T): T { 23 | this.forEach { if (predicate(it) != null && predicate(it)!!) return it } 24 | return default 25 | } 26 | 27 | inline fun Iterable.findAndMap(find: (T) -> Boolean, map: (T) -> T): List { 28 | return map { if (find(it)) map(it) else it } 29 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/MainThread.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import android.content.Context 23 | import com.cesarvaliente.kunidirectional.store.ThreadExecutor 24 | import org.jetbrains.anko.runOnUiThread 25 | import java.lang.ref.WeakReference 26 | 27 | class MainThread(val context: WeakReference) : ThreadExecutor { 28 | override fun execute(block: () -> Unit) { 29 | context.get()?.runOnUiThread { block() } 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /app/src/test/kotlin/com/cesarvaliente/kunidirectional/TestStore.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import android.util.Log 23 | import com.cesarvaliente.kunidirectional.store.State 24 | import com.cesarvaliente.kunidirectional.store.Store 25 | 26 | object TestStore : Store( 27 | storeThread = null, 28 | logger = { tag, message -> Log.d(tag, message) }) { 29 | 30 | fun clear() { 31 | sideEffects.clear() 32 | stateHandlers.clear() 33 | state = State() 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/State.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store 21 | 22 | enum class Navigation { 23 | ITEMS_LIST, 24 | EDIT_ITEM 25 | } 26 | 27 | data class ItemsListScreen( 28 | val items: List = emptyList()) 29 | 30 | data class EditItemScreen(val currentItem: Item = Item()) 31 | 32 | data class State( 33 | val itemsListScreen: ItemsListScreen = ItemsListScreen(), 34 | val editItemScreen: EditItemScreen = EditItemScreen(), 35 | val navigation: Navigation = Navigation.ITEMS_LIST) 36 | -------------------------------------------------------------------------------- /app/src/main/res/itemslist/layout/item_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 24 | 25 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/itemslist/layout/items_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/test/kotlin/com/cesarvaliente/kunidirectional/TestUtils.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import com.cesarvaliente.kunidirectional.store.Color 23 | import com.cesarvaliente.kunidirectional.store.Item 24 | 25 | internal val LOCAL_ID = "localId" 26 | internal val TEXT = "new item" 27 | internal val COLOR = Color.RED 28 | internal val FAVORITE = false 29 | internal val POSITION = 1L 30 | 31 | internal fun createItem(index: Int): Item = 32 | Item(localId = LOCAL_ID + index, 33 | text = TEXT + index, 34 | favorite = FAVORITE, 35 | color = COLOR, 36 | position = POSITION + index) -------------------------------------------------------------------------------- /store/src/test/kotlin/com/cesarvaliente/kunidirectional/reducer/ReducerTestUtils.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.Color 23 | import com.cesarvaliente.kunidirectional.store.Item 24 | 25 | internal val LOCAL_ID = "localId" 26 | internal val TEXT = "text" 27 | internal val POSITION = 1L 28 | internal val COLOR = Color.RED 29 | internal val FAVORITE = false 30 | 31 | internal fun createItem(index: Int): Item = 32 | Item(localId = LOCAL_ID + index, 33 | text = TEXT + index, 34 | favorite = FAVORITE, 35 | color = COLOR, 36 | position = POSITION + index) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/AppStore.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import android.util.Log 23 | import com.cesarvaliente.kunidirectional.persistence.PersistenceSideEffect 24 | import com.cesarvaliente.kunidirectional.persistence.PersistenceThreadService 25 | import com.cesarvaliente.kunidirectional.store.Store 26 | import com.cesarvaliente.kunidirectional.store.StoreThreadService 27 | 28 | object AppStore : Store( 29 | storeThread = StoreThreadService(), 30 | logger = { tag, message -> Log.d(tag, message) }) { 31 | 32 | fun enablePersistence() { 33 | PersistenceSideEffect(store = this, persistenceThread = PersistenceThreadService()) 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /persistence/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'realm-android' 5 | 6 | 7 | android { 8 | compileSdkVersion rootProject.ext.compileSdkVersion 9 | buildToolsVersion rootProject.ext.buildToolsVersion 10 | 11 | defaultConfig { 12 | minSdkVersion rootProject.ext.minSdkVersion 13 | targetSdkVersion rootProject.ext.targetSdkVersion 14 | 15 | testInstrumentationRunner rootProject.ext.testInstrumentationRunner 16 | } 17 | 18 | sourceSets { 19 | main.java.srcDirs += 'src/main/kotlin' 20 | test.java.srcDirs += 'src/test/kotlin' 21 | androidTest.java.srcDirs += 'src/androidTest/kotlin' 22 | } 23 | } 24 | 25 | dependencies { 26 | compile project(':store') 27 | 28 | compile kotlinDependencies.kotlinStdlib 29 | compile kotlinDependencies.kotlinReflect 30 | 31 | androidTestCompile supportDependencies.supportAnnotations 32 | androidTestCompile instrumentationTestDependencies.testRunner 33 | androidTestCompile instrumentationTestDependencies.testRules 34 | androidTestCompile unitTestDependencies.mockito 35 | androidTestCompile unitTestDependencies.dexmakerMockito 36 | androidTestCompile unitTestDependencies.mockitoKotlin 37 | 38 | testCompile unitTestDependencies.junit 39 | testCompile unitTestDependencies.mockito 40 | testCompile unitTestDependencies.hamcrestLibrary 41 | testCompile unitTestDependencies.hamcrestCore 42 | } 43 | -------------------------------------------------------------------------------- /persistence/src/test/kotlin/com/cesarvaliente/kunidirectional/persistence/PersistenceSideEffectTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence 21 | 22 | import com.cesarvaliente.kunidirectional.store.Store 23 | import org.junit.Assert.assertThat 24 | import org.junit.Test 25 | import org.hamcrest.CoreMatchers.`is` as iz 26 | 27 | class PersistenceSideEffectTest { 28 | val store = object : Store() {} 29 | 30 | @Test 31 | fun should_subscribe_to_store() { 32 | val persistenceActionSubscriber = PersistenceSideEffect( 33 | store = store) 34 | 35 | with(store.sideEffects) { 36 | assertThat(isEmpty(), iz(false)) 37 | assertThat(contains(persistenceActionSubscriber), iz(true)) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /store/src/test/kotlin/com/cesarvaliente/kunidirectional/PositionsFactoryTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import com.cesarvaliente.kunidirectional.store.PositionsFactory 23 | import org.hamcrest.CoreMatchers.not 24 | import org.hamcrest.Matchers.greaterThan 25 | import org.junit.Assert.assertThat 26 | import org.junit.Test 27 | import org.hamcrest.CoreMatchers.`is` as iz 28 | 29 | class PositionsFactoryTest { 30 | 31 | @Test 32 | fun should_return_new_position() { 33 | val positionsFactory = object : PositionsFactory {} 34 | val position1 = positionsFactory.newPosition() 35 | val position2 = positionsFactory.newPosition() 36 | 37 | assertThat(position1, iz(not(position2))) 38 | assertThat(position2, greaterThan(position1)) 39 | } 40 | } -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/Models.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store 21 | 22 | import java.util.UUID 23 | 24 | const val LOCAL_ID = "localId" 25 | 26 | interface PositionsFactory { 27 | fun newPosition() = System.nanoTime() 28 | } 29 | 30 | fun generateLocalId(): String = LOCAL_ID + "_" + UUID.randomUUID().toString().replace("-".toRegex(), "") 31 | 32 | enum class Color { 33 | RED, YELLOW, GREEN, BLUE, WHITE 34 | } 35 | 36 | data class Item( 37 | val localId: String = generateLocalId(), 38 | val text: String? = null, 39 | val favorite: Boolean = false, 40 | val color: Color = Color.WHITE, 41 | val position: Long = object : PositionsFactory {}.newPosition()) { 42 | 43 | fun isEmpty(): Boolean = text == null 44 | 45 | fun isNotEmpty(): Boolean = !isEmpty() 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/itemslist/recyclerview/ItemsDiffCallback.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.itemslist.recyclerview 21 | 22 | import android.support.v7.util.DiffUtil 23 | import com.cesarvaliente.kunidirectional.store.Item 24 | 25 | class ItemsDiffCallback(val oldItems: List, val newItems: List) : DiffUtil.Callback() { 26 | 27 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 28 | return oldItems[oldItemPosition].localId == newItems[newItemPosition].localId 29 | } 30 | 31 | override fun getOldListSize(): Int = oldItems.size 32 | 33 | override fun getNewListSize(): Int = newItems.size 34 | 35 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 36 | return oldItems[oldItemPosition] == newItems[newItemPosition] 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/ItemsApplication.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import android.app.Application 23 | import android.preference.PreferenceManager 24 | import com.cesarvaliente.kunidirectional.persistence.setupPersistence 25 | 26 | class ItemsApplication : Application() { 27 | 28 | override fun onCreate() { 29 | super.onCreate() 30 | storeInitialization() 31 | } 32 | 33 | private fun storeInitialization() { 34 | if (isPersistenceEnabled()) { 35 | AppStore.enablePersistence() 36 | setupPersistence(context = this) 37 | } 38 | } 39 | 40 | private fun isPersistenceEnabled(): Boolean { 41 | val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) 42 | return sharedPreferences.getBoolean(getString(R.string.pref_persistence_key), true) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/Subscribers.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store 21 | 22 | import java.util.concurrent.CopyOnWriteArrayList 23 | 24 | abstract class Subscriber(private val executeOnThisThread: ThreadExecutor? = null) { 25 | 26 | fun onNext(data: T) { 27 | executeOnThisThread?.execute { handle(data) } ?: handle(data) 28 | } 29 | 30 | abstract fun handle(data: T) 31 | } 32 | 33 | abstract class SideEffect(executeOnThisThread: ThreadExecutor? = null) : Subscriber(executeOnThisThread) 34 | 35 | abstract class StateHandler(executeOnThisThread: ThreadExecutor? = null) : Subscriber(executeOnThisThread) 36 | 37 | fun CopyOnWriteArrayList.dispatch(action: Action) { 38 | forEach { it.onNext(action) } 39 | } 40 | 41 | fun CopyOnWriteArrayList.dispatch(state: State) { 42 | forEach { it.onNext(state) } 43 | } -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/reducer/DeleteReducer.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.Item 23 | import com.cesarvaliente.kunidirectional.store.DeleteAction 24 | import com.cesarvaliente.kunidirectional.store.DeleteAction.DeleteItemAction 25 | 26 | 27 | object DeleteReducer : Reducer() { 28 | 29 | override fun reduceItemsCollection(action: DeleteAction, currentItems: List): List = 30 | when (action) { 31 | is DeleteItemAction -> currentItems.filterNot { it.localId == action.localId } 32 | } 33 | 34 | override fun reduceCurrentItem(action: DeleteAction, currentItem: Item): Item = 35 | when (action) { 36 | is DeleteItemAction -> if (action.localId == currentItem.localId) Item() else currentItem 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/reducer/CreationReducer.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.Item 23 | import com.cesarvaliente.kunidirectional.store.CreationAction 24 | import com.cesarvaliente.kunidirectional.store.CreationAction.CreateItemAction 25 | 26 | object CreationReducer : Reducer() { 27 | 28 | override fun reduceItemsCollection(action: CreationAction, currentItems: List): List = 29 | when (action) { 30 | is CreateItemAction -> currentItems + createNewItem(action) 31 | } 32 | 33 | private fun createNewItem(action: CreateItemAction): Item = 34 | with(action) { 35 | Item(localId = localId, 36 | text = text, 37 | favorite = favorite, 38 | color = color, 39 | position = position) 40 | } 41 | } -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/reducer/ReadReducer.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.Item 23 | import com.cesarvaliente.kunidirectional.store.Navigation 24 | import com.cesarvaliente.kunidirectional.store.ReadAction 25 | import com.cesarvaliente.kunidirectional.store.ReadAction.ItemsLoadedAction 26 | 27 | object ReadReducer : Reducer() { 28 | 29 | override fun reduceItemsCollection(action: ReadAction, currentItems: List): List = 30 | when (action) { 31 | is ItemsLoadedAction -> action.items 32 | else -> super.reduceItemsCollection(action, currentItems) 33 | } 34 | 35 | override fun reduceCurrentItem(action: ReadAction, currentItem: Item): Item = 36 | Item() 37 | 38 | override fun reduceNavigation(action: ReadAction, currentNavigation: Navigation): Navigation = 39 | Navigation.ITEMS_LIST 40 | 41 | } -------------------------------------------------------------------------------- /persistence/src/main/kotlin/com/cesarvaliente/kunidirectional/persistence/handler/DeleteHandler.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence.handler 21 | 22 | import com.cesarvaliente.kunidirectional.persistence.delete 23 | import com.cesarvaliente.kunidirectional.persistence.queryByLocalId 24 | import com.cesarvaliente.kunidirectional.store.Action 25 | import com.cesarvaliente.kunidirectional.store.DeleteAction 26 | import com.cesarvaliente.kunidirectional.store.DeleteAction.DeleteItemAction 27 | import io.realm.Realm 28 | 29 | object DeleteHandler : ActionHandler { 30 | 31 | override fun handle(action: DeleteAction, actionDispatcher: (Action) -> Unit) { 32 | when (action) { 33 | is DeleteItemAction -> deleteItem(action) 34 | } 35 | } 36 | 37 | private fun deleteItem(action: DeleteItemAction) { 38 | val db = Realm.getDefaultInstance() 39 | val managedItem = db.queryByLocalId(action.localId) 40 | managedItem?.delete(db) 41 | db.close() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/ViewActivity.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import android.support.v7.app.AppCompatActivity 23 | 24 | abstract class ViewActivity : AppCompatActivity() { 25 | 26 | lateinit var controllerView: T 27 | 28 | override fun onStart() { 29 | super.onStart() 30 | controllerView.onStart() 31 | } 32 | 33 | override fun onPause() { 34 | super.onPause() 35 | controllerView.onPause() 36 | } 37 | 38 | override fun onResume() { 39 | super.onResume() 40 | controllerView.onResume() 41 | } 42 | 43 | override fun onStop() { 44 | super.onStop() 45 | controllerView.onStop() 46 | } 47 | 48 | override fun onDestroy() { 49 | super.onDestroy() 50 | controllerView.onDestroy() 51 | } 52 | 53 | protected fun registerControllerViewForLifecycle(controllerView: T) { 54 | this.controllerView = controllerView 55 | } 56 | 57 | abstract fun setupControllerView() 58 | } 59 | -------------------------------------------------------------------------------- /store/src/test/kotlin/com/cesarvaliente/kunidirectional/ModelsTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import com.cesarvaliente.kunidirectional.store.Item 23 | import com.cesarvaliente.kunidirectional.store.generateLocalId 24 | import org.hamcrest.CoreMatchers.containsString 25 | import org.hamcrest.CoreMatchers.not 26 | import org.junit.Assert.assertThat 27 | import org.junit.Test 28 | import org.hamcrest.CoreMatchers.`is` as iz 29 | 30 | class ModelsTest { 31 | 32 | @Test 33 | fun should_generateLocalId() { 34 | val localId1 = generateLocalId() 35 | 36 | assertThat(localId1, not(containsString("-"))) 37 | } 38 | 39 | @Test 40 | fun should_generate_different_localId() { 41 | val localId1 = generateLocalId() 42 | val localId2 = generateLocalId() 43 | 44 | assertThat(localId1, iz(not(localId2))) 45 | } 46 | 47 | @Test 48 | fun should_Item_be_empty() { 49 | val item = Item() 50 | 51 | assertThat(item.isEmpty(), iz(true)) 52 | } 53 | 54 | @Test 55 | fun should_item_not_be_empty() { 56 | val item = Item(text = "test") 57 | 58 | assertThat(item.isEmpty(), iz(false)) 59 | } 60 | } -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/reducer/NavigationReducer.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.EditItemScreen 23 | import com.cesarvaliente.kunidirectional.store.Navigation 24 | import com.cesarvaliente.kunidirectional.store.NavigationAction 25 | import com.cesarvaliente.kunidirectional.store.NavigationAction.EditItemScreenAction 26 | import com.cesarvaliente.kunidirectional.store.NavigationAction.ItemsScreenAction 27 | 28 | object NavigationReducer : Reducer() { 29 | 30 | override fun reduceEditItemScreen(action: NavigationAction, editItemScreen: EditItemScreen): EditItemScreen = 31 | when (action) { 32 | is EditItemScreenAction -> editItemScreen.copy( 33 | currentItem = action.item) 34 | else -> super.reduceEditItemScreen(action, editItemScreen) 35 | } 36 | 37 | override fun reduceNavigation(action: NavigationAction, currentNavigation: Navigation): Navigation = 38 | when (action) { 39 | is EditItemScreenAction -> Navigation.EDIT_ITEM 40 | is ItemsScreenAction -> Navigation.ITEMS_LIST 41 | } 42 | } -------------------------------------------------------------------------------- /persistence/src/main/kotlin/com/cesarvaliente/kunidirectional/persistence/handler/ReadHandler.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence.handler 21 | 22 | import com.cesarvaliente.kunidirectional.persistence.queryAllItemsSortedByPosition 23 | import com.cesarvaliente.kunidirectional.persistence.toStoreItemsList 24 | import com.cesarvaliente.kunidirectional.store.Action 25 | import com.cesarvaliente.kunidirectional.store.ReadAction 26 | import com.cesarvaliente.kunidirectional.store.ReadAction.FetchItemsAction 27 | import com.cesarvaliente.kunidirectional.store.ReadAction.ItemsLoadedAction 28 | import io.realm.Realm 29 | 30 | object ReadHandler : ActionHandler { 31 | 32 | override fun handle(action: ReadAction, actionDispatcher: (Action) -> Unit) { 33 | when (action) { 34 | is FetchItemsAction -> fetchAllItems(actionDispatcher) 35 | } 36 | } 37 | 38 | private fun fetchAllItems(actionDispatcher: (Action) -> Unit) { 39 | val db = Realm.getDefaultInstance() 40 | val persistenceItems = db.queryAllItemsSortedByPosition() 41 | val storeItems = persistenceItems.toStoreItemsList() 42 | db.close() 43 | 44 | actionDispatcher.invoke(ItemsLoadedAction(storeItems)) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/ControllerView.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import com.cesarvaliente.kunidirectional.store.Item 23 | import com.cesarvaliente.kunidirectional.store.State 24 | import com.cesarvaliente.kunidirectional.store.StateHandler 25 | import com.cesarvaliente.kunidirectional.store.Store 26 | import com.cesarvaliente.kunidirectional.store.ThreadExecutor 27 | 28 | abstract class ControllerView( 29 | val store: Store, 30 | mainThread: ThreadExecutor? = null) 31 | : LifecycleCallbacks, StateHandler(mainThread) { 32 | 33 | override var isActivityRunning: Boolean = false 34 | 35 | val state: State 36 | get() = store.state 37 | 38 | val currentItem: Item 39 | get() = state.editItemScreen.currentItem 40 | 41 | override fun onStart() { 42 | isActivityRunning = true 43 | store.stateHandlers.add(this) 44 | handleState(store.state) 45 | } 46 | 47 | override fun onResume() {} 48 | 49 | override fun onPause() {} 50 | 51 | override fun onStop() { 52 | isActivityRunning = false 53 | } 54 | 55 | override fun onDestroy() { 56 | store.stateHandlers.remove(this) 57 | } 58 | 59 | override fun handle(data: State) { 60 | if (isActivityRunning) handleState(state) 61 | } 62 | 63 | abstract fun handleState(state: State) 64 | } 65 | -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/Actions.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store 21 | 22 | sealed class Action 23 | 24 | sealed class NavigationAction : Action() { 25 | data class EditItemScreenAction(val item: Item) : NavigationAction() 26 | class ItemsScreenAction : NavigationAction() 27 | } 28 | 29 | sealed class ReadAction : Action() { 30 | class FetchItemsAction : ReadAction() 31 | data class ItemsLoadedAction(val items: List) : ReadAction() 32 | } 33 | 34 | sealed class CreationAction : Action() { 35 | data class CreateItemAction(val localId: String, val text: String, val favorite: Boolean = false, 36 | val color: Color, val position: Long) : CreationAction() 37 | } 38 | 39 | sealed class UpdateAction : Action() { 40 | data class ReorderItemsAction(val items: List) : UpdateAction() 41 | 42 | data class UpdateItemAction(val localId: String, 43 | val text: String, 44 | val color: Color) : UpdateAction() 45 | 46 | data class UpdateFavoriteAction(val localId: String, val favorite: Boolean) : UpdateAction() 47 | 48 | data class UpdateColorAction(val localId: String, val color: Color) : UpdateAction() 49 | } 50 | 51 | sealed class DeleteAction : Action() { 52 | data class DeleteItemAction(val localId: String) : DeleteAction() 53 | } 54 | -------------------------------------------------------------------------------- /persistence/src/main/kotlin/com/cesarvaliente/kunidirectional/persistence/handler/CreationHandler.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence.handler 21 | 22 | import com.cesarvaliente.kunidirectional.persistence.Item 23 | import com.cesarvaliente.kunidirectional.persistence.insertOrUpdate 24 | import com.cesarvaliente.kunidirectional.persistence.toPersistenceColor 25 | import com.cesarvaliente.kunidirectional.store.Action 26 | import com.cesarvaliente.kunidirectional.store.CreationAction 27 | import com.cesarvaliente.kunidirectional.store.CreationAction.CreateItemAction 28 | import io.realm.Realm 29 | 30 | object CreationHandler : ActionHandler { 31 | 32 | override fun handle(action: CreationAction, actionDispatcher: (Action) -> Unit) { 33 | when (action) { 34 | is CreateItemAction -> createItem(action) 35 | } 36 | } 37 | 38 | private fun createItem(action: CreateItemAction) = 39 | with(action) { 40 | val item = Item( 41 | localId = localId, 42 | text = text, 43 | favorite = favorite, 44 | colorEnum = color.toPersistenceColor(), 45 | position = position) 46 | 47 | val db = Realm.getDefaultInstance() 48 | item.insertOrUpdate(db) 49 | db.close() 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/Threading.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store 21 | 22 | import java.util.concurrent.ExecutorService 23 | import java.util.concurrent.Executors 24 | import java.util.concurrent.ThreadFactory 25 | import java.util.concurrent.atomic.AtomicInteger 26 | 27 | interface ThreadExecutor { 28 | fun execute(block: () -> Unit) 29 | } 30 | 31 | abstract class ThreadExecutorService(open val executorService: ExecutorService) : ThreadExecutor { 32 | override fun execute(block: () -> Unit) { 33 | executorService.execute { block() } 34 | } 35 | } 36 | 37 | class StoreThreadService : ThreadExecutorService(ExecutorServices.store) 38 | 39 | object ExecutorServices { 40 | 41 | val store: ExecutorService by lazy { 42 | store() 43 | } 44 | 45 | private fun store(): ExecutorService = 46 | Executors.newSingleThreadExecutor(NamedThreadFactory("store")) 47 | 48 | val persistence: ExecutorService by lazy { 49 | persistence() 50 | } 51 | 52 | private fun persistence(): ExecutorService = 53 | Executors.newSingleThreadExecutor(NamedThreadFactory("persistence")) 54 | } 55 | 56 | class NamedThreadFactory(private val name: String) : ThreadFactory { 57 | 58 | private val threadNumber = AtomicInteger(1) 59 | 60 | override fun newThread(runnable: Runnable): Thread { 61 | return Thread(runnable, "$name - ${threadNumber.andIncrement}") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /store/src/test/kotlin/com/cesarvaliente/kunidirectional/IterableExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import com.cesarvaliente.kunidirectional.store.findAndMap 23 | import com.cesarvaliente.kunidirectional.store.firstOrDefault 24 | import org.hamcrest.CoreMatchers.hasItem 25 | import org.hamcrest.CoreMatchers.not 26 | import org.junit.Assert.assertThat 27 | import org.junit.Test 28 | import org.hamcrest.CoreMatchers.`is` as iz 29 | 30 | class IterableExtensionsTest { 31 | 32 | @Test 33 | fun should_return_first() { 34 | val dummyList = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 35 | val element = dummyList.firstOrDefault(predicate = { it % 4 == 0 }, default = 10) 36 | 37 | assertThat(element, iz(4)) 38 | } 39 | 40 | @Test 41 | fun should_return_default() { 42 | val dummyList = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 43 | val element = dummyList.firstOrDefault(predicate = { it % 12 == 0 }, default = 10) 44 | 45 | assertThat(element, iz(10)) 46 | } 47 | 48 | @Test 49 | fun should_find_and_map() { 50 | val dummyList = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 51 | val dummyListMapped = dummyList.findAndMap(find = { it % 2 == 0 }, map = { it * 10 }) 52 | 53 | assertThat(dummyListMapped, hasItem(20)) 54 | } 55 | 56 | @Test 57 | fun should_not_find_and_not_map() { 58 | val dummyList = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 59 | val dummyListMapped = dummyList.findAndMap(find = { it % 12 == 0 }, map = { it * 10 }) 60 | 61 | assertThat(dummyListMapped, not(hasItem(20))) 62 | } 63 | } -------------------------------------------------------------------------------- /store/src/test/kotlin/com/cesarvaliente/kunidirectional/reducer/ReadReducerTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.ReadAction 23 | import com.cesarvaliente.kunidirectional.store.reducer.ReadReducer 24 | import org.hamcrest.CoreMatchers.not 25 | import org.junit.Assert.assertThat 26 | import org.junit.Test 27 | import org.hamcrest.CoreMatchers.`is` as iz 28 | 29 | 30 | class ReadReducerTest { 31 | 32 | @Test 33 | fun should_reduceItemsCollection_when_ItemsLoadedCollection_return_new_items_list() { 34 | val item1 = createItem(1) 35 | val item2 = createItem(2) 36 | val item3 = createItem(3) 37 | val listOfItems = listOf(item1, item2, item3) 38 | val itemsLoadedAction = ReadAction.ItemsLoadedAction(listOfItems) 39 | 40 | val reducedItemsCollection = ReadReducer.reduceItemsCollection(itemsLoadedAction, emptyList()) 41 | assertThat(reducedItemsCollection, iz(not(emptyList()))) 42 | assertThat(reducedItemsCollection.size, iz(3)) 43 | assertThat(reducedItemsCollection, iz(listOfItems)) 44 | } 45 | 46 | @Test 47 | fun should_reduceItemsCollection_when_FetchItemsAction_return_unmodified_items_list() { 48 | val item1 = createItem(1) 49 | val item2 = createItem(2) 50 | val item3 = createItem(3) 51 | val listOfItems = listOf(item1, item2, item3) 52 | val itemsLoadedAction = ReadAction.FetchItemsAction() 53 | 54 | val reducedItemsCollection = ReadReducer.reduceItemsCollection(itemsLoadedAction, listOfItems) 55 | assertThat(reducedItemsCollection, iz(listOfItems)) 56 | } 57 | } -------------------------------------------------------------------------------- /persistence/src/main/kotlin/com/cesarvaliente/kunidirectional/persistence/Mapper.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence 21 | 22 | import com.cesarvaliente.kunidirectional.persistence.Color as PersistenceColor 23 | import com.cesarvaliente.kunidirectional.persistence.Item as PersistenceItem 24 | import com.cesarvaliente.kunidirectional.store.Color as StoreColor 25 | import com.cesarvaliente.kunidirectional.store.Item as StoreItem 26 | 27 | fun StoreItem.toPersistenceItem(): PersistenceItem = 28 | with(this) { 29 | PersistenceItem(localId, text, favorite, color.toPersistenceColor(), position) 30 | } 31 | 32 | fun StoreColor.toPersistenceColor(): PersistenceColor = 33 | when (this) { 34 | StoreColor.BLUE -> PersistenceColor.BLUE 35 | StoreColor.GREEN -> PersistenceColor.GREEN 36 | StoreColor.RED -> PersistenceColor.RED 37 | StoreColor.WHITE -> PersistenceColor.WHITE 38 | StoreColor.YELLOW -> PersistenceColor.YELLOW 39 | } 40 | 41 | fun PersistenceColor.toStoreColor(): StoreColor = 42 | when (this) { 43 | PersistenceColor.BLUE -> StoreColor.BLUE 44 | PersistenceColor.GREEN -> StoreColor.GREEN 45 | PersistenceColor.RED -> StoreColor.RED 46 | PersistenceColor.WHITE -> StoreColor.WHITE 47 | PersistenceColor.YELLOW -> StoreColor.YELLOW 48 | } 49 | 50 | fun PersistenceItem.toStoreItem(): StoreItem = 51 | with(this) { 52 | StoreItem(localId, text, favorite, getColorAsEnum().toStoreColor(), position) 53 | } 54 | 55 | fun List.toStoreItemsList(): List = 56 | this.map(PersistenceItem::toStoreItem) 57 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/itemslist/recyclerview/ItemTouchHelperCallback.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.itemslist.recyclerview 21 | 22 | import android.support.v7.widget.RecyclerView 23 | import android.support.v7.widget.helper.ItemTouchHelper 24 | 25 | class ItemTouchHelperCallback(val itemsAdapter: ItemsAdapter) 26 | : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START) { 27 | 28 | var isDragging = false 29 | 30 | override fun onMove(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { 31 | itemsAdapter.onItemMove(fromPosition = viewHolder.adapterPosition, toPosition = target.adapterPosition) 32 | return true 33 | } 34 | 35 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { 36 | if (direction == ItemTouchHelper.START) { 37 | itemsAdapter.onItemDeleted(viewHolder.adapterPosition) 38 | } 39 | } 40 | 41 | override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { 42 | if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder is ItemTouchHelperViewHolder) { 43 | isDragging = true 44 | viewHolder.onItemSelected() 45 | } 46 | super.onSelectedChanged(viewHolder, actionState) 47 | } 48 | 49 | override fun clearView(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?) { 50 | if (viewHolder is ItemTouchHelperViewHolder && isDragging) { 51 | isDragging = false 52 | viewHolder.onItemClear() 53 | } 54 | super.clearView(recyclerView, viewHolder) 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /persistence/src/test/kotlin/com/cesarvaliente/kunidirectional/persistence/ModelsTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence 21 | 22 | import org.hamcrest.CoreMatchers.not 23 | import org.junit.Assert.assertThat 24 | import org.junit.Test 25 | import org.hamcrest.CoreMatchers.`is` as iz 26 | 27 | class ModelsTest { 28 | private val LOCAL_ID = "localId" 29 | private val TEXT = "text" 30 | private val POSITION = 1L 31 | private val COLOR = Color.RED 32 | private val FAVORITE = false 33 | 34 | 35 | @Test 36 | fun should_parse_Color_correctly_with_default_value() { 37 | val item = Item() 38 | assertThat(item.getColorAsEnum(), iz(Color.WHITE)) 39 | } 40 | 41 | @Test 42 | fun should_parse_Color_correctly_from_setter() { 43 | val item = Item() 44 | item.setColorAsEnum(Color.BLUE) 45 | assertThat(item.color.toUpperCase(), iz(Color.BLUE.name)) 46 | } 47 | 48 | @Test 49 | fun should_Item_be_equal_to_other() { 50 | val item1 = Item(localId = LOCAL_ID, text = TEXT, 51 | favorite = FAVORITE, colorEnum = COLOR, position = POSITION) 52 | val item2 = Item(localId = LOCAL_ID, text = TEXT, 53 | favorite = FAVORITE, colorEnum = COLOR, position = POSITION) 54 | 55 | assertThat(item1, iz(item2)) 56 | } 57 | 58 | @Test 59 | fun should_Item_not_be_equal_to_other() { 60 | val item1 = Item(localId = LOCAL_ID, text = TEXT, 61 | favorite = FAVORITE, colorEnum = COLOR, position = POSITION) 62 | val item2 = Item(localId = LOCAL_ID + 1, text = TEXT + 1, 63 | favorite = FAVORITE, colorEnum = COLOR, position = POSITION) 64 | 65 | assertThat(item1, iz(not(item2))) 66 | } 67 | } -------------------------------------------------------------------------------- /persistence/src/androidTest/kotlin/com/cesarvaliente/kunidirectional/persistence/TestUtils.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence 21 | 22 | import org.junit.Assert.assertThat 23 | import com.cesarvaliente.kunidirectional.persistence.Color as PersistenceColor 24 | import com.cesarvaliente.kunidirectional.persistence.Item as PersistenceItem 25 | import com.cesarvaliente.kunidirectional.store.Color as StoreColor 26 | import com.cesarvaliente.kunidirectional.store.Item as StoreItem 27 | import org.hamcrest.core.Is.`is` as iz 28 | 29 | const val LOCAL_ID_VALUE = "localId" 30 | const val TEXT_VALUE = "text" 31 | const val POSITION_VALUE = 1L 32 | val COLOR_VALUE = com.cesarvaliente.kunidirectional.store.Color.RED 33 | const val FAVORITE_VALUE = false 34 | 35 | 36 | fun createPersistenceItem(index: Int): PersistenceItem = 37 | createStoreItem(index).toPersistenceItem() 38 | 39 | fun createStoreItem(index: Int): StoreItem = 40 | StoreItem( 41 | localId = LOCAL_ID_VALUE + index, 42 | text = TEXT_VALUE + index, 43 | favorite = FAVORITE_VALUE, 44 | color = COLOR_VALUE, 45 | position = POSITION_VALUE + index) 46 | 47 | /** 48 | This function asserts that the current item is the same the given item. 49 | We can not use equals() from Item, since a result from Realm is not a real Item, but 50 | a proxy that matches our Item, so equals() always fails since we are comparing Item with a ProxyItem */ 51 | fun PersistenceItem.assertIsEqualsTo(otherItem: PersistenceItem) { 52 | assertThat(localId, iz(otherItem.localId)) 53 | assertThat(text, iz(otherItem.text)) 54 | assertThat(color, iz(otherItem.color)) 55 | assertThat(favorite, iz(otherItem.favorite)) 56 | assertThat(position, iz(otherItem.position)) 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /persistence/src/main/kotlin/com/cesarvaliente/kunidirectional/persistence/PersistenceSideEffect.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence 21 | 22 | import com.cesarvaliente.kunidirectional.persistence.handler.CreationHandler 23 | import com.cesarvaliente.kunidirectional.persistence.handler.DeleteHandler 24 | import com.cesarvaliente.kunidirectional.persistence.handler.ReadHandler 25 | import com.cesarvaliente.kunidirectional.persistence.handler.UpdateHandler 26 | import com.cesarvaliente.kunidirectional.store.Action 27 | import com.cesarvaliente.kunidirectional.store.CreationAction 28 | import com.cesarvaliente.kunidirectional.store.DeleteAction 29 | import com.cesarvaliente.kunidirectional.store.ExecutorServices 30 | import com.cesarvaliente.kunidirectional.store.ReadAction 31 | import com.cesarvaliente.kunidirectional.store.SideEffect 32 | import com.cesarvaliente.kunidirectional.store.Store 33 | import com.cesarvaliente.kunidirectional.store.ThreadExecutor 34 | import com.cesarvaliente.kunidirectional.store.ThreadExecutorService 35 | import com.cesarvaliente.kunidirectional.store.UpdateAction 36 | 37 | class PersistenceThreadService : ThreadExecutorService(ExecutorServices.persistence) 38 | 39 | class PersistenceSideEffect(val store: Store, persistenceThread: ThreadExecutor? = null) 40 | : SideEffect(persistenceThread) { 41 | 42 | init { 43 | store.sideEffects.add(this) 44 | } 45 | 46 | override fun handle(action: Action) { 47 | println("Persistence thread: ${Thread.currentThread().name}") 48 | when (action) { 49 | is CreationAction -> CreationHandler.handle(action) { store.dispatch(it) } 50 | is UpdateAction -> UpdateHandler.handle(action) { store.dispatch(it) } 51 | is ReadAction -> ReadHandler.handle(action) { store.dispatch(it) } 52 | is DeleteAction -> DeleteHandler.handle(action) { store.dispatch(it) } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /persistence/src/main/kotlin/com/cesarvaliente/kunidirectional/persistence/PersistenceFunctions.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence 21 | 22 | import android.content.Context 23 | import io.realm.Realm 24 | import io.realm.RealmConfiguration 25 | import io.realm.RealmModel 26 | import io.realm.RealmObject 27 | import io.realm.RealmResults 28 | 29 | fun setupPersistence(context: Context, dbName: String = context.getString(R.string.database_name)) { 30 | configureDb(context, dbName) 31 | } 32 | 33 | private fun configureDb(context: Context, dbName: String = context.getString(R.string.database_name)) { 34 | Realm.init(context) 35 | val realmConfig = RealmConfiguration.Builder() 36 | .name(dbName) 37 | .deleteRealmIfMigrationNeeded() 38 | .build() 39 | Realm.setDefaultConfiguration(realmConfig) 40 | } 41 | 42 | fun Realm.queryAllItemsSortedByPosition(): RealmResults = 43 | this.where(Item::class.java).findAll().sort(POSITION) 44 | 45 | fun Realm.queryByLocalId(id: String): Item? = 46 | this.where(Item::class.java).equalTo(LOCAL_ID, id).findFirst() 47 | 48 | fun Item.insertOrUpdate(db: Realm): Item { 49 | val managedItem = db.insertOrUpdateInTransaction(this) 50 | return managedItem 51 | } 52 | 53 | fun Realm.insertOrUpdateInTransaction(model: T): T = 54 | with(this) { 55 | beginTransaction() 56 | val managedItem = copyToRealmOrUpdate(model) 57 | commitTransaction() 58 | return managedItem 59 | } 60 | 61 | fun Item.update(db: Realm, changes: (Item.() -> Unit)): Item { 62 | executeTransaction(db) { this.changes() } 63 | return this 64 | } 65 | 66 | private fun executeTransaction(db: Realm, changes: () -> Unit) { 67 | db.executeTransaction { changes() } 68 | } 69 | 70 | fun Item.delete(db: Realm) { 71 | executeTransaction(db) { RealmObject.deleteFromRealm(this) } 72 | } 73 | -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/reducer/UpdateReducer.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.Item 23 | import com.cesarvaliente.kunidirectional.store.UpdateAction 24 | import com.cesarvaliente.kunidirectional.store.UpdateAction.ReorderItemsAction 25 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateColorAction 26 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateFavoriteAction 27 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateItemAction 28 | 29 | object UpdateReducer : Reducer() { 30 | 31 | override fun reduceItemsCollection(action: UpdateAction, currentItems: List): List = 32 | when (action) { 33 | is ReorderItemsAction -> action.items 34 | else -> super.reduceItemsCollection(action, currentItems) 35 | } 36 | 37 | override fun shouldReduceItem(action: UpdateAction, currentItem: Item): Boolean = 38 | when (action) { 39 | is UpdateItemAction -> action.localId == currentItem.localId 40 | is UpdateFavoriteAction -> action.localId == currentItem.localId 41 | is UpdateColorAction -> action.localId == currentItem.localId 42 | else -> false 43 | } 44 | 45 | override fun changeItemFields(action: UpdateAction, currentItem: Item): Item = 46 | when (action) { 47 | is UpdateItemAction -> currentItem.copy( 48 | text = action.text, 49 | color = action.color) 50 | is UpdateFavoriteAction -> currentItem.copy( 51 | favorite = action.favorite 52 | ) 53 | is UpdateColorAction -> currentItem.copy( 54 | color = action.color 55 | ) 56 | else -> currentItem 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/itemslist/ItemsControllerView.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.itemslist 21 | 22 | import com.cesarvaliente.kunidirectional.ControllerView 23 | import com.cesarvaliente.kunidirectional.store.DeleteAction.DeleteItemAction 24 | import com.cesarvaliente.kunidirectional.store.Item 25 | import com.cesarvaliente.kunidirectional.store.Navigation 26 | import com.cesarvaliente.kunidirectional.store.NavigationAction.EditItemScreenAction 27 | import com.cesarvaliente.kunidirectional.store.ReadAction.FetchItemsAction 28 | import com.cesarvaliente.kunidirectional.store.State 29 | import com.cesarvaliente.kunidirectional.store.Store 30 | import com.cesarvaliente.kunidirectional.store.ThreadExecutor 31 | import com.cesarvaliente.kunidirectional.store.UpdateAction.ReorderItemsAction 32 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateFavoriteAction 33 | import java.lang.ref.WeakReference 34 | 35 | class ItemsControllerView( 36 | val itemsViewCallback: WeakReference, 37 | store: Store, 38 | mainThread: ThreadExecutor? = null) 39 | : ControllerView(store, mainThread) { 40 | 41 | fun fetchItems() = 42 | store.dispatch(FetchItemsAction()) 43 | 44 | fun toEditItemScreen(item: Item) = 45 | store.dispatch(EditItemScreenAction(item)) 46 | 47 | fun reorderItems(items: List) = 48 | store.dispatch(ReorderItemsAction(items)) 49 | 50 | fun changeFavoriteStatus(item: Item) = 51 | store.dispatch(UpdateFavoriteAction(localId = item.localId, favorite = !item.favorite)) 52 | 53 | fun deleteItem(item: Item) = 54 | store.dispatch(DeleteItemAction(item.localId)) 55 | 56 | override fun handleState(state: State) { 57 | when (state.navigation) { 58 | Navigation.ITEMS_LIST -> itemsViewCallback.get()?.updateItems(state.itemsListScreen.items) 59 | Navigation.EDIT_ITEM -> itemsViewCallback.get()?.goToEditItem() 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/edititem/EditItemControllerView.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.edititem 21 | 22 | import com.cesarvaliente.kunidirectional.ControllerView 23 | import com.cesarvaliente.kunidirectional.store.Color 24 | import com.cesarvaliente.kunidirectional.store.CreationAction.CreateItemAction 25 | import com.cesarvaliente.kunidirectional.store.Navigation 26 | import com.cesarvaliente.kunidirectional.store.NavigationAction.ItemsScreenAction 27 | import com.cesarvaliente.kunidirectional.store.State 28 | import com.cesarvaliente.kunidirectional.store.Store 29 | import com.cesarvaliente.kunidirectional.store.ThreadExecutor 30 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateColorAction 31 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateItemAction 32 | import java.lang.ref.WeakReference 33 | 34 | class EditItemControllerView( 35 | val editItemViewCallback: WeakReference, 36 | store: Store, 37 | mainThread: ThreadExecutor? = null) 38 | : ControllerView(store, mainThread) { 39 | 40 | fun createItem(localId: String, text: String, favorite: Boolean, color: Color, position: Long) = 41 | store.dispatch(CreateItemAction(localId, text, favorite, color, position)) 42 | 43 | fun updateItem(localId: String, text: String, color: Color) = 44 | store.dispatch(UpdateItemAction(localId, text, color)) 45 | 46 | fun updateColor(localId: String, color: Color) = 47 | store.dispatch(UpdateColorAction(localId, color)) 48 | 49 | fun backToItems() = 50 | store.dispatch(ItemsScreenAction()) 51 | 52 | override fun handleState(state: State) { 53 | println("Thread hadleState: ${Thread.currentThread().name}") 54 | when (state.navigation) { 55 | Navigation.EDIT_ITEM -> editItemViewCallback.get()?.updateItem(state.editItemScreen.currentItem) 56 | Navigation.ITEMS_LIST -> editItemViewCallback.get()?.backToItemsList() 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /store/src/test/kotlin/com/cesarvaliente/kunidirectional/reducer/NavigationReducerTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.EditItemScreen 23 | import com.cesarvaliente.kunidirectional.store.Item 24 | import com.cesarvaliente.kunidirectional.store.Navigation 25 | import com.cesarvaliente.kunidirectional.store.NavigationAction 26 | import com.cesarvaliente.kunidirectional.store.reducer.NavigationReducer 27 | import org.hamcrest.CoreMatchers.not 28 | import org.junit.Assert.assertThat 29 | import org.junit.Test 30 | import org.hamcrest.CoreMatchers.`is` as iz 31 | 32 | class NavigationReducerTest { 33 | 34 | @Test 35 | fun should_reduceEditItemScreen_when_EditItemScreenAction() { 36 | val defaultItem = Item() 37 | val editItemScreen = EditItemScreen(currentItem = Item()) 38 | 39 | val item1 = createItem(1) 40 | val editItemScreenAction = NavigationAction.EditItemScreenAction(item = item1) 41 | val reducedEditItemScreen = NavigationReducer.reduceEditItemScreen(editItemScreenAction, editItemScreen) 42 | 43 | assertThat(reducedEditItemScreen.currentItem, iz(not(defaultItem))) 44 | assertThat(reducedEditItemScreen.currentItem, iz(item1)) 45 | } 46 | 47 | @Test 48 | fun should_reduceNavigation_when_EditItemScreenAction_change_to_EDIT_ITEM() { 49 | val item1 = createItem(1) 50 | val editItemScreenAction = NavigationAction.EditItemScreenAction(item = item1) 51 | 52 | val reducedNavigation = NavigationReducer.reduceNavigation(editItemScreenAction, Navigation.ITEMS_LIST) 53 | assertThat(reducedNavigation, iz(Navigation.EDIT_ITEM)) 54 | } 55 | 56 | @Test 57 | fun should_reduceNavigation_when_ItemsScreenAction_change_to_ITEMS_LIST() { 58 | val editItemScreenAction = NavigationAction.ItemsScreenAction() 59 | 60 | val reducedNavigation = NavigationReducer.reduceNavigation(editItemScreenAction, Navigation.EDIT_ITEM) 61 | assertThat(reducedNavigation, iz(Navigation.ITEMS_LIST)) 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/res/edititem/layout/edit_item_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 30 | 31 | 38 | 39 | 46 | 47 | 54 | 55 | 62 | 63 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/reducer/Reducer.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.EditItemScreen 23 | import com.cesarvaliente.kunidirectional.store.Item 24 | import com.cesarvaliente.kunidirectional.store.ItemsListScreen 25 | import com.cesarvaliente.kunidirectional.store.Navigation 26 | import com.cesarvaliente.kunidirectional.store.State 27 | import com.cesarvaliente.kunidirectional.store.Action 28 | import com.cesarvaliente.kunidirectional.store.findAndMap 29 | 30 | abstract class Reducer { 31 | 32 | open fun reduce(action: T, currentState: State) = 33 | with(currentState) { 34 | currentState.copy( 35 | itemsListScreen = reduceItemsListScreen(action, itemsListScreen), 36 | editItemScreen = reduceEditItemScreen(action, editItemScreen), 37 | navigation = reduceNavigation(action, navigation) 38 | ) 39 | } 40 | 41 | open fun reduceItemsListScreen(action: T, itemsListScreen: ItemsListScreen) = 42 | itemsListScreen.copy(items = reduceItemsCollection(action, itemsListScreen.items)) 43 | 44 | open fun reduceItemsCollection(action: T, currentItems: List) = 45 | currentItems.findAndMap( 46 | find = { shouldReduceItem(action, it) }, 47 | map = { changeItemFields(action, it) }) 48 | 49 | open fun reduceEditItemScreen(action: T, editItemScreen: EditItemScreen) = 50 | editItemScreen.copy( 51 | currentItem = reduceCurrentItem(action, editItemScreen.currentItem)) 52 | 53 | open fun reduceCurrentItem(action: T, currentItem: Item) = 54 | if (shouldReduceItem(action, currentItem)) changeItemFields(action, currentItem) 55 | else currentItem 56 | 57 | open fun shouldReduceItem(action: T, currentItem: Item) = false 58 | 59 | open fun changeItemFields(action: T, currentItem: Item) = currentItem 60 | 61 | open fun reduceNavigation(action: T, currentNavigation: Navigation) = currentNavigation 62 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/Extensions.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import android.support.v4.content.ContextCompat.getColor 23 | import android.view.View 24 | import android.widget.EditText 25 | import android.widget.ImageView 26 | import com.cesarvaliente.kunidirectional.store.Color 27 | import com.cesarvaliente.kunidirectional.store.Color.BLUE 28 | import com.cesarvaliente.kunidirectional.store.Color.GREEN 29 | import com.cesarvaliente.kunidirectional.store.Color.RED 30 | import com.cesarvaliente.kunidirectional.store.Color.WHITE 31 | import com.cesarvaliente.kunidirectional.store.Color.YELLOW 32 | import com.cesarvaliente.kunidirectional.store.Item 33 | 34 | fun ImageView.load(drawableId: Int) = 35 | //Here we could use an Image library to load our resource on a different way having it isolated 36 | setImageResource(drawableId) 37 | 38 | 39 | fun View.changeBackgroundColor(colorId: Int) = 40 | setBackgroundColor(getColor(context, colorId)) 41 | 42 | fun View.changeBackgroundColor(color: Color) = 43 | setBackgroundColor(getColor(context, color.toColorResource())) 44 | 45 | fun Color.toColorResource(): Int = 46 | //We don't use Presentation models, we enrich the Store models using EF 47 | when (this) { 48 | RED -> R.color.red 49 | YELLOW -> R.color.yellow 50 | GREEN -> R.color.green 51 | BLUE -> R.color.blue 52 | WHITE -> R.color.white 53 | } 54 | 55 | fun Item.getStableId(): Long = localId.hashCode().toLong() 56 | 57 | fun EditText.isNotBlankThen(blockTextNotBlank: () -> Unit, 58 | blockTextBlank: (() -> Unit)? = null) { 59 | if (isNotBlank()) { 60 | blockTextNotBlank() 61 | } else { 62 | blockTextBlank?.invoke() 63 | } 64 | } 65 | 66 | fun EditText.updateText(newText: String?) = 67 | newText?.let { 68 | if (isNotBlank() && isDifferentThan(it)) { 69 | setText(it) 70 | } 71 | } 72 | 73 | fun EditText.isDifferentThan(newText: String): Boolean = 74 | text.toString() != newText 75 | 76 | fun EditText.isNotBlank(): Boolean = 77 | text.isNotBlank() -------------------------------------------------------------------------------- /persistence/src/main/kotlin/com/cesarvaliente/kunidirectional/persistence/Models.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence 21 | 22 | import io.realm.RealmModel 23 | import io.realm.annotations.Ignore 24 | import io.realm.annotations.PrimaryKey 25 | import io.realm.annotations.RealmClass 26 | 27 | 28 | internal const val LOCAL_ID = "localId" 29 | internal const val POSITION = "position" 30 | 31 | enum class Color { 32 | RED, YELLOW, GREEN, BLUE, WHITE 33 | } 34 | 35 | @RealmClass 36 | open class Item() : RealmModel { 37 | constructor(localId: String, text: String?, favorite: Boolean = false, 38 | colorEnum: Color = Color.WHITE, position: Long) : this() { 39 | this.localId = localId 40 | this.text = text 41 | this.favorite = favorite 42 | this.color = colorEnum.name 43 | this.position = position 44 | } 45 | 46 | @PrimaryKey open var localId: String = "" 47 | open var text: String? = null 48 | open var favorite: Boolean = false 49 | @Ignore private var colorEnum: Color = Color.WHITE 50 | open var color: String = colorEnum.name 51 | open var position: Long = 0 52 | 53 | fun getColorAsEnum(): Color = Color.valueOf(color) 54 | 55 | fun setColorAsEnum(color: Color) { 56 | this.color = color.name 57 | } 58 | 59 | override fun equals(other: Any?): Boolean { 60 | if (this === other) return true 61 | if (other?.javaClass != javaClass) return false 62 | 63 | other as Item 64 | 65 | if (localId != other.localId) return false 66 | if (text != other.text) return false 67 | if (favorite != other.favorite) return false 68 | if (colorEnum != other.colorEnum) return false 69 | if (color != other.color) return false 70 | if (position != other.position) return false 71 | 72 | return true 73 | } 74 | 75 | override fun hashCode(): Int { 76 | var result = localId.hashCode() 77 | result = 31 * result + (text?.hashCode() ?: 0) 78 | result = 31 * result + favorite.hashCode() 79 | result = 31 * result + colorEnum.hashCode() 80 | result = 31 * result + color.hashCode() 81 | result = 31 * result + position.hashCode() 82 | return result 83 | } 84 | } -------------------------------------------------------------------------------- /persistence/src/androidTest/kotlin/com/cesarvaliente/kunidirectional/persistence/handler/DeleteHandlerTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence.handler 21 | 22 | import android.support.test.InstrumentationRegistry 23 | import android.support.test.runner.AndroidJUnit4 24 | import com.cesarvaliente.kunidirectional.persistence.createPersistenceItem 25 | import com.cesarvaliente.kunidirectional.persistence.delete 26 | import com.cesarvaliente.kunidirectional.persistence.insertOrUpdate 27 | import com.cesarvaliente.kunidirectional.persistence.queryAllItemsSortedByPosition 28 | import io.realm.Realm 29 | import io.realm.RealmConfiguration 30 | import org.hamcrest.CoreMatchers.not 31 | import org.junit.After 32 | import org.junit.Assert.assertThat 33 | import org.junit.Before 34 | import org.junit.Test 35 | import org.junit.runner.RunWith 36 | import com.cesarvaliente.kunidirectional.persistence.Color as PersistenceColor 37 | import com.cesarvaliente.kunidirectional.persistence.Item as PersistenceItem 38 | import com.cesarvaliente.kunidirectional.store.Color as StoreColor 39 | import com.cesarvaliente.kunidirectional.store.Item as StoreItem 40 | import org.hamcrest.core.Is.`is` as iz 41 | 42 | @RunWith(AndroidJUnit4::class) 43 | class DeleteHandlerTest { 44 | lateinit var config: RealmConfiguration 45 | lateinit var db: Realm 46 | 47 | @Before 48 | fun setup() { 49 | Realm.init(InstrumentationRegistry.getTargetContext()) 50 | config = RealmConfiguration.Builder() 51 | .name("test.realm") 52 | .inMemory() 53 | .build() 54 | Realm.setDefaultConfiguration(config) 55 | db = Realm.getInstance(config) 56 | } 57 | 58 | @After 59 | fun clean() { 60 | db.close() 61 | Realm.deleteRealm(config) 62 | } 63 | 64 | @Test 65 | fun should_delete_Item() { 66 | val item = createPersistenceItem(1) 67 | val managedItem = item.insertOrUpdate(db) 68 | 69 | var itemsCollection = db.queryAllItemsSortedByPosition() 70 | assertThat(itemsCollection, iz(not(emptyList()))) 71 | assertThat(itemsCollection.size, iz(1)) 72 | 73 | managedItem.delete(db) 74 | itemsCollection = db.queryAllItemsSortedByPosition() 75 | assertThat(itemsCollection, iz(emptyList())) 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/itemslist/recyclerview/ItemViewHolder.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.itemslist.recyclerview 21 | 22 | import android.support.v7.widget.RecyclerView 23 | import android.view.View 24 | import android.widget.ImageView 25 | import android.widget.RelativeLayout 26 | import android.widget.TextView 27 | import com.cesarvaliente.kunidirectional.R 28 | import com.cesarvaliente.kunidirectional.changeBackgroundColor 29 | import com.cesarvaliente.kunidirectional.load 30 | import com.cesarvaliente.kunidirectional.store.Item 31 | import com.cesarvaliente.kunidirectional.toColorResource 32 | import org.jetbrains.anko.find 33 | 34 | internal interface ItemTouchHelperViewHolder { 35 | fun onItemSelected() 36 | fun onItemClear() 37 | } 38 | 39 | class ItemViewHolder(view: View, 40 | private val itemClick: (Item) -> Unit, 41 | private val setFavorite: (Item) -> Unit, 42 | private val reorderItems: () -> Unit) 43 | : RecyclerView.ViewHolder(view), ItemTouchHelperViewHolder { 44 | 45 | private lateinit var currentItem: Item 46 | val itemContentLayout: RelativeLayout = itemView.find(R.id.contentLayout) 47 | val itemText: TextView = itemView.find(R.id.itemText) 48 | val itemStar: ImageView = itemView.find(R.id.itemStar) 49 | 50 | fun bindItem(item: Item) = 51 | with(item) { 52 | currentItem = this 53 | bindViewContent(this) 54 | bindClickHandlers(this) 55 | } 56 | 57 | private fun bindViewContent(item: Item) = 58 | with(item) { 59 | itemText.text = text 60 | itemStar.load( 61 | if (favorite) R.drawable.ic_star_black 62 | else R.drawable.ic_star_border_black) 63 | itemContentLayout.changeBackgroundColor(color.toColorResource()) 64 | } 65 | 66 | private fun bindClickHandlers(item: Item) { 67 | itemContentLayout.setOnClickListener { itemClick(item) } 68 | itemStar.setOnClickListener { setFavorite(item) } 69 | } 70 | 71 | override fun onItemSelected() { 72 | itemContentLayout.changeBackgroundColor(R.color.item_selected) 73 | } 74 | 75 | override fun onItemClear() { 76 | itemContentLayout.changeBackgroundColor(currentItem.color.toColorResource()) 77 | reorderItems() 78 | } 79 | } -------------------------------------------------------------------------------- /store/src/test/kotlin/com/cesarvaliente/kunidirectional/reducer/CreationReducerTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.Item 23 | import com.cesarvaliente.kunidirectional.store.LOCAL_ID 24 | import com.cesarvaliente.kunidirectional.store.CreationAction.CreateItemAction 25 | import com.cesarvaliente.kunidirectional.store.reducer.CreationReducer 26 | import org.hamcrest.CoreMatchers.not 27 | import org.junit.Assert.assertThat 28 | import org.junit.Test 29 | import org.hamcrest.CoreMatchers.`is` as iz 30 | 31 | 32 | class CreationReducerTest { 33 | @Test 34 | fun should_reduceItemsCollection_when_CreateItemAction_and_empty_collection() { 35 | val createItemAction = CreateItemAction( 36 | localId = LOCAL_ID, 37 | text = TEXT, 38 | favorite = FAVORITE, 39 | color = COLOR, 40 | position = POSITION) 41 | 42 | val itemsReduced = CreationReducer.reduceItemsCollection( 43 | action = createItemAction, 44 | currentItems = emptyList()) 45 | 46 | assertThat(itemsReduced, iz(not(emptyList()))) 47 | assertThat(itemsReduced.count(), iz(1)) 48 | assertThat(itemsReduced, iz(listOf( 49 | Item(localId = LOCAL_ID, 50 | text = TEXT, favorite = FAVORITE, color = COLOR, 51 | position = POSITION)))) 52 | } 53 | 54 | @Test 55 | fun should_reduceItemsCollection_when_CreateItemAction_and_non_empty_collection() { 56 | val item1 = createItem(1) 57 | val item2 = createItem(2) 58 | 59 | val listOfItems = listOf(item1, item2) 60 | 61 | val action = CreateItemAction( 62 | localId = LOCAL_ID + 3, 63 | text = TEXT + 3, 64 | favorite = FAVORITE, 65 | color = COLOR, 66 | position = POSITION + 3) 67 | 68 | val itemsReduced = CreationReducer.reduceItemsCollection( 69 | action = action, 70 | currentItems = listOfItems) 71 | 72 | assertThat(itemsReduced, iz(not(emptyList()))) 73 | assertThat(itemsReduced.count(), iz(3)) 74 | assertThat(itemsReduced, iz(listOf( 75 | item1, item2, Item(localId = LOCAL_ID + 3, 76 | text = TEXT + 3, favorite = FAVORITE, color = COLOR, position = POSITION + 3)))) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion rootProject.ext.compileSdkVersion 7 | buildToolsVersion rootProject.ext.buildToolsVersion 8 | 9 | def Properties versionProps = readVersionProps() 10 | defaultConfig { 11 | minSdkVersion rootProject.ext.minSdkVersion 12 | targetSdkVersion rootProject.ext.targetSdkVersion 13 | 14 | versionName versionProps['name'] 15 | versionCode versionProps['code'].toInteger() 16 | applicationId versionProps['applicationId'] 17 | 18 | testInstrumentationRunner rootProject.ext.customTestInstrumentationRunner 19 | vectorDrawables.useSupportLibrary = true 20 | } 21 | 22 | testOptions { 23 | unitTests.returnDefaultValues = true 24 | } 25 | 26 | //In a feature set scheme, here we can have our resources grouped by them 27 | sourceSets { 28 | main { 29 | res.srcDirs += [ 30 | 'src/main/res/main', 31 | 'src/main/res/itemslist', 32 | 'src/main/res/edititem'] 33 | java.srcDirs += 'src/main/kotlin' 34 | } 35 | test.java.srcDirs += 'src/test/kotlin' 36 | androidTest.java.srcDirs += 'src/androidTest/kotlin' 37 | } 38 | 39 | lintOptions { 40 | abortOnError false 41 | } 42 | 43 | buildTypes { 44 | release { 45 | minifyEnabled false 46 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 47 | } 48 | } 49 | } 50 | 51 | dependencies { 52 | compile project(':persistence') 53 | compile project(':store') 54 | 55 | //Kotlin & anko 56 | compile kotlinDependencies.kotlinStdlib 57 | compile ankoDependencies.ankoCommon 58 | 59 | //Android Support Library 60 | compile supportDependencies.supportCoreUiV4 61 | compile supportDependencies.supportFragmentV4 62 | compile supportDependencies.supportAnnotations 63 | compile supportDependencies.appCompatV7 64 | compile supportDependencies.recyclerViewV7 65 | compile supportDependencies.cardViewV7 66 | compile supportDependencies.supportDesign 67 | 68 | //Instrumentation tests 69 | androidTestCompile supportDependencies.supportAnnotations 70 | androidTestCompile instrumentationTestDependencies.testRunner 71 | androidTestCompile instrumentationTestDependencies.testRules 72 | androidTestCompile instrumentationTestDependencies.espressoCore 73 | androidTestCompile instrumentationTestDependencies.espressoContrib 74 | androidTestCompile instrumentationTestDependencies.espressoIdlingResources 75 | 76 | //Unit tests 77 | testCompile unitTestDependencies.junit 78 | testCompile unitTestDependencies.mockito 79 | testCompile unitTestDependencies.hamcrestLibrary 80 | testCompile unitTestDependencies.hamcrestCore 81 | testCompile unitTestDependencies.robolectric 82 | testCompile unitTestDependencies.mockitoKotlin 83 | } 84 | 85 | def readVersionProps() { 86 | def Properties props = new Properties() 87 | props.load(new FileInputStream(file('../version.properties'))) 88 | return props 89 | } -------------------------------------------------------------------------------- /store/src/main/kotlin/com/cesarvaliente/kunidirectional/store/Store.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.store 21 | 22 | import com.cesarvaliente.kunidirectional.store.reducer.CreationReducer 23 | import com.cesarvaliente.kunidirectional.store.reducer.DeleteReducer 24 | import com.cesarvaliente.kunidirectional.store.reducer.NavigationReducer 25 | import com.cesarvaliente.kunidirectional.store.reducer.ReadReducer 26 | import com.cesarvaliente.kunidirectional.store.reducer.UpdateReducer 27 | import java.util.concurrent.CopyOnWriteArrayList 28 | import java.util.concurrent.LinkedBlockingQueue 29 | 30 | interface Subscribers { 31 | val sideEffects: CopyOnWriteArrayList 32 | val stateHandlers: CopyOnWriteArrayList 33 | 34 | fun dispatch(action: Action) 35 | fun dispatch(state: State) 36 | } 37 | 38 | abstract class Store(override val sideEffects: CopyOnWriteArrayList = CopyOnWriteArrayList(), 39 | override val stateHandlers: CopyOnWriteArrayList = CopyOnWriteArrayList(), 40 | private val storeThread: ThreadExecutor? = null, 41 | private val logger: (String, String) -> Unit = { _, _ -> Unit }) : Subscribers { 42 | 43 | private var actions = LinkedBlockingQueue() 44 | 45 | var state = State() 46 | protected set 47 | 48 | @Synchronized 49 | override fun dispatch(action: Action) { 50 | actions.offer(action) 51 | when { 52 | storeThread != null -> storeThread.execute { handle(actions.poll()) } 53 | else -> handle(actions.poll()) 54 | } 55 | } 56 | 57 | private fun handle(action: Action) { 58 | val newState = reduce(action, state) 59 | dispatch(newState) 60 | sideEffects.dispatch(action) 61 | } 62 | 63 | override fun dispatch(state: State) { 64 | this.state = state 65 | stateHandlers.dispatch(state) 66 | } 67 | 68 | private fun reduce(action: Action, currentState: State): State { 69 | logger("action", action.toString()) 70 | val newState = when (action) { 71 | is CreationAction -> CreationReducer.reduce(action, currentState) 72 | is UpdateAction -> UpdateReducer.reduce(action, currentState) 73 | is ReadAction -> ReadReducer.reduce(action, currentState) 74 | is DeleteAction -> DeleteReducer.reduce(action, currentState) 75 | is NavigationAction -> NavigationReducer.reduce(action, currentState) 76 | } 77 | logger("new state", newState.toString()) 78 | return newState 79 | } 80 | } -------------------------------------------------------------------------------- /persistence/src/androidTest/kotlin/com/cesarvaliente/kunidirectional/persistence/handler/CreationHandlerTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence.handler 21 | 22 | import android.support.test.InstrumentationRegistry 23 | import android.support.test.runner.AndroidJUnit4 24 | import com.cesarvaliente.kunidirectional.persistence.assertIsEqualsTo 25 | import com.cesarvaliente.kunidirectional.persistence.createStoreItem 26 | import com.cesarvaliente.kunidirectional.persistence.queryAllItemsSortedByPosition 27 | import com.cesarvaliente.kunidirectional.persistence.toPersistenceItem 28 | import com.cesarvaliente.kunidirectional.store.CreationAction 29 | import io.realm.Realm 30 | import io.realm.RealmConfiguration 31 | import org.hamcrest.CoreMatchers.not 32 | import org.junit.After 33 | import org.junit.Assert.assertThat 34 | import org.junit.Before 35 | import org.junit.Test 36 | import org.junit.runner.RunWith 37 | import com.cesarvaliente.kunidirectional.persistence.Color as PersistenceColor 38 | import com.cesarvaliente.kunidirectional.persistence.Item as PersistenceItem 39 | import com.cesarvaliente.kunidirectional.store.Color as StoreColor 40 | import com.cesarvaliente.kunidirectional.store.Item as StoreItem 41 | import org.hamcrest.core.Is.`is` as iz 42 | 43 | @RunWith(AndroidJUnit4::class) 44 | class CreationHandlerTest { 45 | lateinit var config: RealmConfiguration 46 | lateinit var db: Realm 47 | 48 | @Before 49 | fun setup() { 50 | Realm.init(InstrumentationRegistry.getTargetContext()) 51 | config = RealmConfiguration.Builder() 52 | .name("test.realm") 53 | .inMemory() 54 | .build() 55 | Realm.setDefaultConfiguration(config) 56 | db = Realm.getInstance(config) 57 | } 58 | 59 | @After 60 | fun clean() { 61 | db.close() 62 | Realm.deleteRealm(config) 63 | } 64 | 65 | @Test 66 | fun should_create_Item() { 67 | val item = createStoreItem(1) 68 | 69 | val createItemAction = with(item) { 70 | CreationAction.CreateItemAction( 71 | localId = localId, 72 | text = text!!, 73 | favorite = favorite, 74 | color = color, 75 | position = position) 76 | } 77 | 78 | CreationHandler.handle(createItemAction, {}) 79 | 80 | val itemsCollection = db.queryAllItemsSortedByPosition() 81 | assertThat(itemsCollection, iz(not(emptyList()))) 82 | assertThat(itemsCollection.size, iz(1)) 83 | 84 | itemsCollection.component1().assertIsEqualsTo(item.toPersistenceItem()) 85 | } 86 | } -------------------------------------------------------------------------------- /persistence/src/main/kotlin/com/cesarvaliente/kunidirectional/persistence/handler/UpdateHandler.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence.handler 21 | 22 | import com.cesarvaliente.kunidirectional.persistence.queryByLocalId 23 | import com.cesarvaliente.kunidirectional.persistence.toPersistenceColor 24 | import com.cesarvaliente.kunidirectional.persistence.update 25 | import com.cesarvaliente.kunidirectional.store.Action 26 | import com.cesarvaliente.kunidirectional.store.UpdateAction 27 | import com.cesarvaliente.kunidirectional.store.UpdateAction.ReorderItemsAction 28 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateColorAction 29 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateFavoriteAction 30 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateItemAction 31 | import io.realm.Realm 32 | 33 | object UpdateHandler : ActionHandler { 34 | 35 | override fun handle(action: UpdateAction, actionDispatcher: (Action) -> Unit) { 36 | when (action) { 37 | is ReorderItemsAction -> reorderItems(action) 38 | is UpdateItemAction -> updateItem(action) 39 | is UpdateFavoriteAction -> updateFavorite(action) 40 | is UpdateColorAction -> updateColor(action) 41 | } 42 | } 43 | 44 | private fun reorderItems(action: ReorderItemsAction) { 45 | if (action.items.isEmpty()) return 46 | 47 | val db = Realm.getDefaultInstance() 48 | action.items.forEach { item -> 49 | val managedItem = db.queryByLocalId(item.localId) 50 | managedItem?.update(db) { position = item.position } 51 | } 52 | db.close() 53 | } 54 | 55 | private fun updateItem(action: UpdateItemAction) { 56 | val db = Realm.getDefaultInstance() 57 | val managedItem = db.queryByLocalId(action.localId) 58 | managedItem?.update(db) { 59 | text = action.text 60 | setColorAsEnum(action.color.toPersistenceColor()) 61 | } 62 | db.close() 63 | } 64 | 65 | private fun updateFavorite(action: UpdateFavoriteAction) { 66 | val db = Realm.getDefaultInstance() 67 | val managedItem = db.queryByLocalId(action.localId) 68 | managedItem?.update(db) { 69 | favorite = action.favorite 70 | } 71 | db.close() 72 | } 73 | 74 | private fun updateColor(action: UpdateColorAction) { 75 | val db = Realm.getDefaultInstance() 76 | val managedItem = db.queryByLocalId(action.localId) 77 | managedItem?.update(db) { 78 | setColorAsEnum(action.color.toPersistenceColor()) 79 | } 80 | db.close() 81 | } 82 | } -------------------------------------------------------------------------------- /store/src/test/kotlin/com/cesarvaliente/kunidirectional/reducer/DeleteReducerTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.DeleteAction 23 | import com.cesarvaliente.kunidirectional.store.reducer.DeleteReducer 24 | import org.hamcrest.CoreMatchers.not 25 | import org.junit.Assert.assertThat 26 | import org.junit.Test 27 | import org.hamcrest.CoreMatchers.`is` as iz 28 | 29 | 30 | class DeleteReducerTest { 31 | 32 | @Test 33 | fun should_reduceItemsCollection_when_DeleteItemAction_item_is_not_in_list() { 34 | val item1 = createItem(1) 35 | val listOfItems = listOf(item1) 36 | 37 | val deleteItemAction = DeleteAction.DeleteItemAction(LOCAL_ID + 2) 38 | val collectionReduced = DeleteReducer.reduceItemsCollection(deleteItemAction, listOfItems) 39 | 40 | assertThat(collectionReduced, iz(not(emptyList()))) 41 | assertThat(collectionReduced.size, iz(1)) 42 | assertThat(collectionReduced[0], iz(item1)) 43 | } 44 | 45 | @Test 46 | fun should_reduceItemsCollection_when_DeleteItemAction_and_returns_empty_list() { 47 | val item1 = createItem(1) 48 | val listOfItems = listOf(item1) 49 | 50 | val deleteItemAction = DeleteAction.DeleteItemAction(item1.localId) 51 | val collectionReduced = DeleteReducer.reduceItemsCollection(deleteItemAction, listOfItems) 52 | 53 | assertThat(collectionReduced, iz(emptyList())) 54 | } 55 | 56 | @Test 57 | fun should_reduceItemsCollection_when_DeleteItemAction_and_returns_non_empty_list() { 58 | val item1 = createItem(1) 59 | val item2 = createItem(2) 60 | val listOfItems = listOf(item1, item2) 61 | 62 | val deleteItemAction = DeleteAction.DeleteItemAction(item1.localId) 63 | val collectionReduced = DeleteReducer.reduceItemsCollection(deleteItemAction, listOfItems) 64 | 65 | assertThat(collectionReduced, iz(not(emptyList()))) 66 | assertThat(collectionReduced.size, iz(1)) 67 | assertThat(collectionReduced[0], iz(item2)) 68 | } 69 | 70 | @Test 71 | fun should_reduceCurrentItem_when_DeleteItemAction_and_items_are_same() { 72 | val item1 = createItem(1) 73 | val deleteItemAction = DeleteAction.DeleteItemAction(item1.localId) 74 | 75 | val itemReduced = DeleteReducer.reduceCurrentItem(deleteItemAction, item1) 76 | assertThat(itemReduced, iz(not(item1))) 77 | assertThat(itemReduced.isEmpty(), iz(true)) 78 | } 79 | 80 | @Test 81 | fun should_reduceCurrentItem_when_DeleteItemAction_and_items_are_not_same() { 82 | val item1 = createItem(1) 83 | val deleteItemAction = DeleteAction.DeleteItemAction(LOCAL_ID + 2) 84 | 85 | val itemReduced = DeleteReducer.reduceCurrentItem(deleteItemAction, item1) 86 | assertThat(itemReduced, iz(item1)) 87 | } 88 | } -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | //Main configuration values 3 | gradlePluginVersion = "2.3.3" 4 | kotlinVersion = "1.1.4-3" 5 | realmVersion = "3.7.2" 6 | compileSdkVersion = 26 7 | buildToolsVersion = '26.0.1' 8 | targetSdkVersion = compileSdkVersion 9 | minSdkVersion = 19 10 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" 11 | customTestInstrumentationRunner = "com.cesarvaliente.kunidirectional.TestRunner" 12 | 13 | //Anko 14 | ankoVersion = '0.10.1' 15 | 16 | // Support Libraries 17 | supportVersion = "26.1.0" 18 | 19 | // Instrumentation test 20 | espressoVersion = "3.0.1" 21 | testRunnerVersion = "1.0.1" 22 | testRulesVersion = "1.0.1" 23 | 24 | //Unit test 25 | junitVersion = "4.12" 26 | mockitoVersion = "2.7.12" 27 | hamcrestVersion = "1.3" 28 | robolectricVersion = "3.2.2" 29 | dexmakerMockitoVersion = "2.2.0" 30 | mockitoKotlinVersion = "1.3.0" 31 | 32 | config = [ 33 | gradlePlugin : "com.android.tools.build:gradle:$gradlePluginVersion", 34 | kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion", 35 | kotlinAndroidExtensions: "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion", 36 | realmPlugin : "io.realm:realm-gradle-plugin:$realmVersion" 37 | ] 38 | 39 | kotlinDependencies = [ 40 | kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion", 41 | kotlinReflect: "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" 42 | ] 43 | 44 | ankoDependencies = [ 45 | ankoCommon: "org.jetbrains.anko:anko-common:$ankoVersion" 46 | ] 47 | 48 | supportDependencies = [ 49 | supportCoreUiV4 : "com.android.support:support-core-ui:${supportVersion}", 50 | supportFragmentV4 : "com.android.support:support-fragment:${supportVersion}", 51 | supportAnnotations: "com.android.support:support-annotations:${supportVersion}", 52 | appCompatV7 : "com.android.support:appcompat-v7:${supportVersion}", 53 | recyclerViewV7 : "com.android.support:recyclerview-v7:${supportVersion}", 54 | cardViewV7 : "com.android.support:cardview-v7:${supportVersion}", 55 | supportDesign : "com.android.support:design:${supportVersion}", 56 | supportCoreUtils : "com.android.support:support-core-utils:${supportVersion}" 57 | ] 58 | 59 | instrumentationTestDependencies = [ 60 | espressoCore : "com.android.support.test.espresso:espresso-core:${espressoVersion}", 61 | espressoContrib : "com.android.support.test.espresso:espresso-contrib:${espressoVersion}", 62 | espressoIdlingResources: "com.android.support.test.espresso:espresso-idling-resource:${espressoVersion}", 63 | testRunner : "com.android.support.test:runner:${testRunnerVersion}", 64 | testRules : "com.android.support.test:rules:${testRulesVersion}" 65 | ] 66 | 67 | unitTestDependencies = [ 68 | junit : "junit:junit:${junitVersion}", 69 | mockito : "org.mockito:mockito-core:${mockitoVersion}", 70 | hamcrestLibrary: "org.hamcrest:hamcrest-library:${hamcrestVersion}", 71 | hamcrestCore : "org.hamcrest:hamcrest-core:${hamcrestVersion}", 72 | robolectric : "org.robolectric:robolectric:${robolectricVersion}", 73 | dexmakerMockito: "com.linkedin.dexmaker:dexmaker-mockito:${dexmakerMockitoVersion}", 74 | mockitoKotlin : "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}" 75 | ] 76 | } -------------------------------------------------------------------------------- /persistence/src/androidTest/kotlin/com/cesarvaliente/kunidirectional/persistence/handler/ReadHandlerTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence.handler 21 | 22 | import android.support.test.InstrumentationRegistry 23 | import android.support.test.runner.AndroidJUnit4 24 | import com.cesarvaliente.kunidirectional.persistence.createPersistenceItem 25 | import com.cesarvaliente.kunidirectional.persistence.insertOrUpdate 26 | import com.cesarvaliente.kunidirectional.persistence.toStoreItem 27 | import com.cesarvaliente.kunidirectional.store.Action 28 | import com.cesarvaliente.kunidirectional.store.ReadAction 29 | import com.cesarvaliente.kunidirectional.store.ReadAction.FetchItemsAction 30 | import com.nhaarman.mockito_kotlin.argumentCaptor 31 | import com.nhaarman.mockito_kotlin.mock 32 | import com.nhaarman.mockito_kotlin.verify 33 | import io.realm.Realm 34 | import io.realm.RealmConfiguration 35 | import org.hamcrest.CoreMatchers.not 36 | import org.junit.After 37 | import org.junit.Assert.assertThat 38 | import org.junit.Before 39 | import org.junit.Test 40 | import org.junit.runner.RunWith 41 | import com.cesarvaliente.kunidirectional.persistence.Color as PersistenceColor 42 | import com.cesarvaliente.kunidirectional.persistence.Item as PersistenceItem 43 | import com.cesarvaliente.kunidirectional.store.Color as StoreColor 44 | import com.cesarvaliente.kunidirectional.store.Item as StoreItem 45 | import org.hamcrest.core.Is.`is` as iz 46 | 47 | @RunWith(AndroidJUnit4::class) 48 | class ReadHandlerTest { 49 | lateinit var config: RealmConfiguration 50 | lateinit var db: Realm 51 | 52 | @Before 53 | fun setup() { 54 | Realm.init(InstrumentationRegistry.getTargetContext()) 55 | config = RealmConfiguration.Builder() 56 | .name("test.realm") 57 | .inMemory() 58 | .build() 59 | Realm.setDefaultConfiguration(config) 60 | db = Realm.getInstance(config) 61 | } 62 | 63 | @After 64 | fun clean() { 65 | db.close() 66 | Realm.deleteRealm(config) 67 | } 68 | 69 | @Test 70 | fun should_fetch_all_Items() { 71 | val item1 = createPersistenceItem(1) 72 | val item2 = createPersistenceItem(2) 73 | val item3 = createPersistenceItem(3) 74 | 75 | item1.insertOrUpdate(db) 76 | item2.insertOrUpdate(db) 77 | item3.insertOrUpdate(db) 78 | 79 | val fetchItemsAction = FetchItemsAction() 80 | val actionDispatcherSpy = mock<(Action) -> Unit> { } 81 | 82 | ReadHandler.handle(action = fetchItemsAction, actionDispatcher = actionDispatcherSpy) 83 | argumentCaptor().apply { 84 | verify(actionDispatcherSpy).invoke(capture()) 85 | 86 | with(lastValue.items) { 87 | assertThat(this, iz(not(emptyList()))) 88 | assertThat(this.size, iz(3)) 89 | 90 | assertThat(component1(), iz(item1.toStoreItem())) 91 | assertThat(component2(), iz(item2.toStoreItem())) 92 | assertThat(component3(), iz(item3.toStoreItem())) 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/itemslist/recyclerview/ItemsAdapter.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.itemslist.recyclerview 21 | 22 | import android.support.v7.util.DiffUtil 23 | import android.support.v7.widget.RecyclerView 24 | import android.view.LayoutInflater 25 | import android.view.ViewGroup 26 | import com.cesarvaliente.kunidirectional.R 27 | import com.cesarvaliente.kunidirectional.getStableId 28 | import com.cesarvaliente.kunidirectional.store.Item 29 | import java.util.Collections 30 | 31 | internal interface ItemTouchHelperAdapter { 32 | fun onItemMove(fromPosition: Int, toPosition: Int) 33 | fun onItemDeleted(position: Int) 34 | } 35 | 36 | class ItemsAdapter( 37 | private var items: List, 38 | private val itemClick: (Item) -> Unit, 39 | private val setFavorite: (Item) -> Unit, 40 | private val updateItemsPositions: (List) -> Unit, 41 | private val deleteItem: (Item) -> Unit) 42 | : RecyclerView.Adapter(), ItemTouchHelperAdapter { 43 | 44 | init { 45 | setHasStableIds(true) 46 | } 47 | 48 | override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ItemViewHolder { 49 | val view = LayoutInflater.from(parent?.context).inflate(R.layout.item_layout, parent, false) 50 | return ItemViewHolder(view, itemClick, setFavorite, this::updateItemsPositions) 51 | } 52 | 53 | override fun onBindViewHolder(itemViewHolder: ItemViewHolder, position: Int) { 54 | itemViewHolder.bindItem(items[position]) 55 | } 56 | 57 | override fun getItemCount(): Int = items.size 58 | 59 | fun getItem(position: Int): Item = items[position] 60 | 61 | override fun getItemId(position: Int): Long = 62 | getItem(position).getStableId() 63 | 64 | fun removeAt(position: Int) { 65 | items = items.minus(items[position]) 66 | notifyItemRemoved(position) 67 | } 68 | 69 | fun updateItems(newItems: List) { 70 | val oldItems = items 71 | items = newItems 72 | applyDiff(oldItems, items) 73 | } 74 | 75 | private fun applyDiff(oldItems: List, newItems: List) { 76 | val diffResult = DiffUtil.calculateDiff(ItemsDiffCallback(oldItems, newItems)) 77 | diffResult.dispatchUpdatesTo(this) 78 | } 79 | 80 | private fun updateItemsPositions() { 81 | updateItemsPositions(items) 82 | } 83 | 84 | override fun onItemDeleted(position: Int) { 85 | deleteItem(items[position]) 86 | removeAt(position) 87 | } 88 | 89 | override fun onItemMove(fromPosition: Int, toPosition: Int) { 90 | swapItems(fromPosition, toPosition) 91 | notifyItemMoved(fromPosition, toPosition) 92 | } 93 | 94 | fun swapItems(fromPosition: Int, toPosition: Int) = if (fromPosition < toPosition) { 95 | (fromPosition .. toPosition - 1).forEach { i -> 96 | swapPositions(i, i + 1) 97 | Collections.swap(items, i, i + 1) 98 | } 99 | } else { 100 | (fromPosition downTo toPosition + 1).forEach { i -> 101 | swapPositions(i, i - 1) 102 | Collections.swap(items, i, i - 1) 103 | } 104 | } 105 | 106 | fun swapPositions(position1: Int, position2: Int) { 107 | val item1 = items[position1] 108 | val item2 = items[position2] 109 | items = items.map { 110 | if (it.localId == item1.localId) it.copy(position = item2.position) 111 | else if (it.localId == item2.localId) it.copy(position = item1.position) 112 | else it 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /app/src/test/kotlin/com/cesarvaliente/kunidirectional/ExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import android.content.Context 23 | import android.os.Build 24 | import android.widget.EditText 25 | import com.cesarvaliente.kunidirectional.store.Color 26 | import org.junit.Assert.assertThat 27 | import org.junit.Assert.fail 28 | import org.junit.Before 29 | import org.junit.Test 30 | import org.junit.runner.RunWith 31 | import org.robolectric.RobolectricTestRunner 32 | import org.robolectric.RuntimeEnvironment 33 | import org.robolectric.annotation.Config 34 | import org.hamcrest.CoreMatchers.`is` as iz 35 | 36 | @RunWith(RobolectricTestRunner::class) 37 | @Config(constants = BuildConfig::class, 38 | sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP), 39 | application = RoboTestApplication::class) 40 | class ExtensionsTest { 41 | lateinit var context: Context 42 | 43 | @Before 44 | fun setup() { 45 | context = RuntimeEnvironment.application 46 | } 47 | 48 | @Test 49 | fun should_parse_Yellow_Color_to_correct_Int_resource() { 50 | assertThat(Color.YELLOW.toColorResource(), iz(R.color.yellow)) 51 | } 52 | 53 | @Test 54 | fun should_parse_Blue_Color_to_correct_Int_resource() { 55 | assertThat(Color.BLUE.toColorResource(), iz(R.color.blue)) 56 | } 57 | 58 | @Test 59 | fun should_parse_Green_Color_to_correct_Int_resource() { 60 | assertThat(Color.GREEN.toColorResource(), iz(R.color.green)) 61 | } 62 | 63 | @Test 64 | fun should_parse_Pink_Color_to_correct_Int_resource() { 65 | assertThat(Color.RED.toColorResource(), iz(R.color.red)) 66 | } 67 | 68 | @Test 69 | fun should_parse_White_Color_to_correct_Int_resource() { 70 | assertThat(Color.WHITE.toColorResource(), iz(R.color.white)) 71 | } 72 | 73 | @Test 74 | fun should_execute_block_not_blank_text_if_EditText_text_is_not_blank() { 75 | val editText = EditText(context) 76 | editText.setText("Hello") 77 | editText.isNotBlankThen(blockTextNotBlank = { assert(true) }, 78 | blockTextBlank = { fail() }) 79 | } 80 | 81 | @Test 82 | fun should_execute_block_text_blank_if_EditText_text_is_blank() { 83 | val editText = EditText(context) 84 | editText.isNotBlankThen(blockTextNotBlank = { fail() }, 85 | blockTextBlank = { assert(true) }) 86 | } 87 | 88 | @Test 89 | fun should_update_text_if_is_different() { 90 | val editText = EditText(context) 91 | editText.setText("Hello") 92 | editText.updateText("World") 93 | assertThat(editText.text.toString(), iz("World")) 94 | } 95 | 96 | @Test 97 | fun should_say_that_is_different_text() { 98 | val editText = EditText(context) 99 | editText.setText("Hello") 100 | assertThat(editText.isDifferentThan("World"), iz(true)) 101 | } 102 | 103 | @Test 104 | fun should_say_that_is_not_different_text() { 105 | val editText = EditText(context) 106 | editText.setText("Hello") 107 | assertThat(editText.isDifferentThan("Hello"), iz(false)) 108 | } 109 | 110 | @Test 111 | fun should_say_text_is_blank_when_not_initialised() { 112 | val editText = EditText(context) 113 | assertThat(editText.isNotBlank(), iz(false)) 114 | } 115 | 116 | @Test 117 | fun should_say_text_is_not_blank_when_has_text() { 118 | val editText = EditText(context) 119 | editText.setText("Hello") 120 | assertThat(editText.isNotBlank(), iz(true)) 121 | } 122 | 123 | @Test 124 | fun should_say_text_is_blank_when_has_text_but_are_blank_spaces() { 125 | val editText = EditText(context) 126 | editText.setText(" ") 127 | assertThat(editText.isNotBlank(), iz(false)) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/test/kotlin/com/cesarvaliente/kunidirectional/ControllerViewTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional 21 | 22 | import com.cesarvaliente.kunidirectional.store.CreationAction.CreateItemAction 23 | import com.cesarvaliente.kunidirectional.store.Item 24 | import com.cesarvaliente.kunidirectional.store.LOCAL_ID 25 | import com.cesarvaliente.kunidirectional.store.Navigation 26 | import com.cesarvaliente.kunidirectional.store.State 27 | import com.nhaarman.mockito_kotlin.argumentCaptor 28 | import com.nhaarman.mockito_kotlin.spy 29 | import com.nhaarman.mockito_kotlin.times 30 | import com.nhaarman.mockito_kotlin.verify 31 | import org.hamcrest.CoreMatchers.not 32 | import org.junit.After 33 | import org.junit.Assert.assertThat 34 | import org.junit.Before 35 | import org.junit.Test 36 | import org.hamcrest.CoreMatchers.`is` as iz 37 | 38 | class ControllerViewTest { 39 | private lateinit var store: TestStore 40 | private lateinit var controllerViewSpy: ControllerView 41 | 42 | @Before 43 | fun setup() { 44 | store = TestStore 45 | val controllerView = object : ControllerView(store = store) { 46 | override var isActivityRunning: Boolean = true 47 | override fun handleState(appState: State) {} 48 | } 49 | controllerViewSpy = spy(controllerView) 50 | store.stateHandlers.add(controllerViewSpy) 51 | } 52 | 53 | @After 54 | fun clean() { 55 | store.clear() 56 | } 57 | 58 | @Test 59 | fun controllerView_should_subscribe_successfully() { 60 | with(store.stateHandlers) { 61 | assertThat(isEmpty(), iz(false)) 62 | assertThat(count(), iz(1)) 63 | assertThat(contains(controllerViewSpy), iz(true)) 64 | } 65 | } 66 | 67 | @Test 68 | fun controllerView_should_unsubscribe() { 69 | with(store.stateHandlers) { 70 | remove(controllerViewSpy) 71 | 72 | assertThat(isEmpty(), iz(true)) 73 | assertThat(contains(controllerViewSpy), iz(false)) 74 | } 75 | } 76 | 77 | @Test 78 | fun should_handle_State() { 79 | val newItem = Item(localId = LOCAL_ID, 80 | text = TEXT, favorite = FAVORITE, color = COLOR, position = POSITION) 81 | 82 | val createItemAction = CreateItemAction(newItem.localId, 83 | newItem.text!!, newItem.favorite, newItem.color, 84 | newItem.position) 85 | 86 | store.dispatch(createItemAction) 87 | 88 | argumentCaptor().apply { 89 | verify(controllerViewSpy).handleState(capture()) 90 | 91 | with(lastValue) { 92 | with(itemsListScreen.items) { 93 | assertThat(this, iz(not(emptyList()))) 94 | assertThat(this.size, iz(1)) 95 | with(this[0]) { 96 | assertThat(this.localId, iz(newItem.localId)) 97 | assertThat(this.text, iz(newItem.text)) 98 | assertThat(this.color, iz(newItem.color)) 99 | assertThat(this.favorite, iz(newItem.favorite)) 100 | assertThat(this.position, iz(newItem.position)) 101 | } 102 | } 103 | assertThat(editItemScreen.currentItem, iz(not(newItem))) 104 | assertThat(navigation, iz(Navigation.ITEMS_LIST)) 105 | } 106 | } 107 | } 108 | 109 | @Test 110 | fun should_not_handle_State_when_activity_is_not_running() { 111 | val newItem = Item(localId = LOCAL_ID, 112 | text = TEXT, favorite = FAVORITE, color = COLOR, position = POSITION) 113 | 114 | val createItemAction = CreateItemAction(newItem.localId, 115 | newItem.text!!, newItem.favorite, newItem.color, 116 | newItem.position) 117 | 118 | controllerViewSpy.isActivityRunning = false 119 | store.dispatch(createItemAction) 120 | 121 | argumentCaptor().apply { 122 | verify(controllerViewSpy, times(0)).handleState(capture()) 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /persistence/src/test/kotlin/com/cesarvaliente/kunidirectional/persistence/MapperTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence 21 | 22 | import org.junit.Assert.assertThat 23 | import org.junit.Test 24 | import com.cesarvaliente.kunidirectional.persistence.Color as PersistenceColor 25 | import com.cesarvaliente.kunidirectional.persistence.Item as PersistenceItem 26 | import com.cesarvaliente.kunidirectional.store.Color as StoreColor 27 | import com.cesarvaliente.kunidirectional.store.Item as StoreItem 28 | import org.hamcrest.CoreMatchers.`is` as iz 29 | 30 | class MapperTest { 31 | private val LOCAL_ID = "localId" 32 | private val TEXT = "text" 33 | private val POSITION = 1L 34 | private val COLOR = StoreColor.RED 35 | private val FAVORITE = false 36 | 37 | @Test 38 | fun should_parse_StoreItem_to_PersistenceItem_correctly() { 39 | val storeItem = StoreItem(localId = LOCAL_ID, text = TEXT, 40 | favorite = FAVORITE, color = COLOR, position = POSITION) 41 | val persistenceItem = storeItem.toPersistenceItem() 42 | 43 | with(persistenceItem) { 44 | assertThat(localId, iz(storeItem.localId)) 45 | assertThat(text, iz(storeItem.text)) 46 | assertThat(favorite, iz(storeItem.favorite)) 47 | assertThat(getColorAsEnum().name, iz(storeItem.color.name)) 48 | assertThat(position, iz(storeItem.position)) 49 | } 50 | } 51 | 52 | @Test 53 | fun should_parse_PersistenceItem_to_StoreItem_correctly() { 54 | val persistenceItem = PersistenceItem(localId = LOCAL_ID, text = TEXT, 55 | favorite = FAVORITE, colorEnum = PersistenceColor.BLUE, position = POSITION) 56 | val storeItem = persistenceItem.toStoreItem() 57 | 58 | with(storeItem) { 59 | assertThat(localId, iz(persistenceItem.localId)) 60 | assertThat(text, iz(persistenceItem.text)) 61 | assertThat(favorite, iz(persistenceItem.favorite)) 62 | assertThat(color.name, iz(persistenceItem.getColorAsEnum().name)) 63 | assertThat(position, iz(persistenceItem.position)) 64 | } 65 | } 66 | 67 | @Test 68 | fun should_parse_StoreColor_BLUE_to_PersistenceColor_correctly() { 69 | val storeColor = StoreColor.BLUE 70 | assertThat(storeColor.name, iz(storeColor.toPersistenceColor().name)) 71 | } 72 | 73 | @Test 74 | fun should_parse_StoreColor_WHITE_to_PersistenceColor_correctly() { 75 | val storeColor = StoreColor.WHITE 76 | assertThat(storeColor.name, iz(storeColor.toPersistenceColor().name)) 77 | } 78 | 79 | @Test 80 | fun should_parse_StoreColor_GREEN_to_PersistenceColor_correctly() { 81 | val storeColor = StoreColor.GREEN 82 | assertThat(storeColor.name, iz(storeColor.toPersistenceColor().name)) 83 | } 84 | 85 | @Test 86 | fun should_parse_StoreColor_RED_to_PersistenceColor_correctly() { 87 | val storeColor = StoreColor.RED 88 | assertThat(storeColor.name, iz(storeColor.toPersistenceColor().name)) 89 | } 90 | 91 | @Test 92 | fun should_parse_StoreColor_YELLOW_to_PersistenceColor_correctly() { 93 | val storeColor = StoreColor.YELLOW 94 | assertThat(storeColor.name, iz(storeColor.toPersistenceColor().name)) 95 | } 96 | 97 | @Test 98 | fun should_parse_PersistenceColor_BLUE_to_StoreColor_correctly() { 99 | val persistenceColor = PersistenceColor.BLUE 100 | assertThat(persistenceColor.name, iz(persistenceColor.toStoreColor().name)) 101 | } 102 | 103 | @Test 104 | fun should_parse_PersistenceColor_WHITE_to_StoreColor_correctly() { 105 | val persistenceColor = PersistenceColor.WHITE 106 | assertThat(persistenceColor.name, iz(persistenceColor.toStoreColor().name)) 107 | } 108 | 109 | @Test 110 | fun should_parse_PersistenceColor_GREEN_to_StoreColor_correctly() { 111 | val persistenceColor = PersistenceColor.GREEN 112 | assertThat(persistenceColor.name, iz(persistenceColor.toStoreColor().name)) 113 | } 114 | 115 | @Test 116 | fun should_parse_PersistenceColor_RED_to_StoreColor_correctly() { 117 | val persistenceColor = PersistenceColor.RED 118 | assertThat(persistenceColor.name, iz(persistenceColor.toStoreColor().name)) 119 | } 120 | 121 | @Test 122 | fun should_parse_PersistenceColor_YELLOW_to_StoreColor_correctly() { 123 | val persistenceColor = PersistenceColor.YELLOW 124 | assertThat(persistenceColor.name, iz(persistenceColor.toStoreColor().name)) 125 | } 126 | } -------------------------------------------------------------------------------- /store/src/test/kotlin/com/cesarvaliente/kunidirectional/reducer/UpdateReducerTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.reducer 21 | 22 | import com.cesarvaliente.kunidirectional.store.Color 23 | import com.cesarvaliente.kunidirectional.store.UpdateAction 24 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateColorAction 25 | import com.cesarvaliente.kunidirectional.store.UpdateAction.UpdateItemAction 26 | import com.cesarvaliente.kunidirectional.store.reducer.UpdateReducer 27 | import org.hamcrest.CoreMatchers.not 28 | import org.junit.Assert.assertThat 29 | import org.junit.Test 30 | import org.hamcrest.CoreMatchers.`is` as iz 31 | 32 | 33 | class UpdateReducerTest { 34 | 35 | @Test 36 | fun should_reduceItemsCollection_when_ReorderItemsAction_return_reorder_items_list() { 37 | val item1 = createItem(1) 38 | val item2 = createItem(2) 39 | val item3 = createItem(3) 40 | 41 | val defaultList = listOf(item2, item3, item1) 42 | val reorderedList = listOf(item1, item2, item3) 43 | 44 | val reorderItemsAction = UpdateAction.ReorderItemsAction(reorderedList) 45 | val reducedItemsCollection = UpdateReducer.reduceItemsCollection(reorderItemsAction, defaultList) 46 | 47 | assertThat(reducedItemsCollection, iz(not(defaultList))) 48 | assertThat(reducedItemsCollection, iz(reorderedList)) 49 | } 50 | 51 | @Test 52 | fun should_shouldReduceItem_when_UpdateItemAction() { 53 | val item = createItem(1) 54 | val updateItemAction = UpdateItemAction(localId = item.localId, text = "new text", color = Color.GREEN) 55 | 56 | val shouldReduceItem = UpdateReducer.shouldReduceItem(updateItemAction, item) 57 | 58 | assertThat(shouldReduceItem, iz(true)) 59 | } 60 | 61 | @Test 62 | fun should_shouldReduceItem_when_UpdateFavoriteAction() { 63 | val item = createItem(1) 64 | val updateFavoriteAction = UpdateAction.UpdateFavoriteAction(localId = item.localId, favorite = true) 65 | 66 | val shouldReduceItem = UpdateReducer.shouldReduceItem(updateFavoriteAction, item) 67 | 68 | assertThat(shouldReduceItem, iz(true)) 69 | } 70 | 71 | @Test 72 | fun should_shouldReduceItem_when_UpdateColorAction() { 73 | val item = createItem(1) 74 | val updateColorAction = UpdateColorAction(localId = item.localId, color = Color.GREEN) 75 | 76 | val shouldReduceItem = UpdateReducer.shouldReduceItem(updateColorAction, item) 77 | 78 | assertThat(shouldReduceItem, iz(true)) 79 | } 80 | 81 | @Test 82 | fun should_not_shouldReduceItem_when_ReorderItemsAction() { 83 | val item1 = createItem(1) 84 | val item2 = createItem(2) 85 | val item3 = createItem(3) 86 | val itemsList = listOf(item1, item2, item3) 87 | 88 | val reorderItemsAction = UpdateAction.ReorderItemsAction(itemsList) 89 | val shouldReduceItem = UpdateReducer.shouldReduceItem(reorderItemsAction, item1) 90 | 91 | assertThat(shouldReduceItem, iz(false)) 92 | } 93 | 94 | @Test 95 | fun should_changeItemFields_when_UpdateItemAction() { 96 | val item = createItem(1) 97 | val NEW_TEXT = "new text" 98 | val updateItemAction = UpdateItemAction(localId = item.localId, text = NEW_TEXT, color = Color.GREEN) 99 | 100 | val reducedItem = UpdateReducer.changeItemFields(updateItemAction, item) 101 | 102 | assertThat(reducedItem, iz(not(item))) 103 | assertThat(reducedItem.text, iz(NEW_TEXT)) 104 | assertThat(reducedItem.color, iz(Color.GREEN)) 105 | } 106 | 107 | @Test 108 | fun should_changeItemFields_when_UpdateFavoriteAction() { 109 | val item = createItem(1) 110 | val updateFavoriteAction = UpdateAction.UpdateFavoriteAction(localId = item.localId, favorite = true) 111 | 112 | val reducedItem = UpdateReducer.changeItemFields(updateFavoriteAction, item) 113 | 114 | assertThat(reducedItem, iz(not(item))) 115 | assertThat(reducedItem.text, iz(item.text)) 116 | assertThat(reducedItem.color, iz(item.color)) 117 | assertThat(reducedItem.favorite, iz(true)) 118 | } 119 | 120 | @Test 121 | fun should_changeItemFields_when_UpdateColorAction() { 122 | val item = createItem(1) 123 | val updateColorAction = UpdateColorAction(localId = item.localId, color = Color.GREEN) 124 | 125 | val reducedItem = UpdateReducer.changeItemFields(updateColorAction, item) 126 | 127 | assertThat(reducedItem, iz(not(item))) 128 | assertThat(reducedItem.text, iz(item.text)) 129 | assertThat(reducedItem.color, iz(Color.GREEN)) 130 | } 131 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /persistence/src/androidTest/kotlin/com/cesarvaliente/kunidirectional/persistence/PersistenceFunctionsTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence 21 | 22 | import android.support.test.InstrumentationRegistry 23 | import android.support.test.runner.AndroidJUnit4 24 | import io.realm.Realm 25 | import io.realm.RealmConfiguration 26 | import org.hamcrest.CoreMatchers.not 27 | import org.hamcrest.CoreMatchers.notNullValue 28 | import org.junit.After 29 | import org.junit.Assert.assertThat 30 | import org.junit.Before 31 | import org.junit.Test 32 | import org.junit.runner.RunWith 33 | import org.hamcrest.core.Is.`is` as iz 34 | 35 | 36 | @RunWith(AndroidJUnit4::class) 37 | class PersistenceFunctionsTest { 38 | lateinit var config: RealmConfiguration 39 | lateinit var db: Realm 40 | 41 | @Before 42 | fun setup() { 43 | Realm.init(InstrumentationRegistry.getTargetContext()) 44 | config = RealmConfiguration.Builder() 45 | .name("test.realm") 46 | .inMemory() 47 | .build() 48 | Realm.setDefaultConfiguration(config) 49 | db = Realm.getInstance(config) 50 | } 51 | 52 | @After 53 | fun clean() { 54 | db.close() 55 | Realm.deleteRealm(config) 56 | } 57 | 58 | @Test 59 | fun should_db_be_empty() { 60 | val itemsCollection = db.queryAllItemsSortedByPosition() 61 | assertThat(itemsCollection, iz(emptyList())) 62 | } 63 | 64 | @Test 65 | fun should_db_not_be_empty() { 66 | val item = createPersistenceItem(1) 67 | item.insertOrUpdate(db) 68 | 69 | val itemsCollection = db.queryAllItemsSortedByPosition() 70 | 71 | assertThat(itemsCollection, iz(not(emptyList()))) 72 | assertThat(itemsCollection.size, iz(1)) 73 | itemsCollection.component1().assertIsEqualsTo(item) 74 | } 75 | 76 | @Test 77 | fun should_query_by_localId_correctly() { 78 | val item = createPersistenceItem(1) 79 | item.insertOrUpdate(db) 80 | 81 | val managedItem = db.queryByLocalId(item.localId) 82 | 83 | assertThat(managedItem, iz(notNullValue())) 84 | managedItem!!.assertIsEqualsTo(item) 85 | } 86 | 87 | @Test 88 | fun should_query_all_items_sorted_by_position() { 89 | val item1 = createPersistenceItem(3) 90 | val item2 = createPersistenceItem(2) 91 | val item3 = createPersistenceItem(1) 92 | 93 | item1.insertOrUpdate(db) 94 | item2.insertOrUpdate(db) 95 | item3.insertOrUpdate(db) 96 | 97 | val itemsCollection = db.queryAllItemsSortedByPosition() 98 | 99 | assertThat(itemsCollection, iz(not(emptyList()))) 100 | assertThat(itemsCollection.size, iz(3)) 101 | itemsCollection.component1().assertIsEqualsTo(item3) 102 | itemsCollection.component2().assertIsEqualsTo(item2) 103 | itemsCollection.component3().assertIsEqualsTo(item1) 104 | } 105 | 106 | @Test 107 | fun should_update_Item_correctly() { 108 | val item = createPersistenceItem(1) 109 | 110 | item.insertOrUpdate(db) 111 | 112 | var managedItem = db.queryByLocalId(item.localId) 113 | assertThat(managedItem, iz(notNullValue())) 114 | managedItem!!.assertIsEqualsTo(item) 115 | 116 | item.text = "Item modified" 117 | item.insertOrUpdate(db) 118 | 119 | managedItem = db.queryByLocalId(item.localId) 120 | assertThat(managedItem, iz(notNullValue())) 121 | assertThat(managedItem!!.text, iz(item.text)) 122 | } 123 | 124 | @Test 125 | fun should_update_Item_defining_changes() { 126 | val item = createPersistenceItem(1) 127 | 128 | val managedItem = item.insertOrUpdate(db) 129 | 130 | var itemsCollection = db.queryAllItemsSortedByPosition() 131 | assertThat(itemsCollection, iz(not(emptyList()))) 132 | assertThat(itemsCollection.size, iz(1)) 133 | 134 | managedItem.update(db) { text = "Item modified" } 135 | 136 | val itemUpdated = db.queryByLocalId(item.localId) 137 | assertThat(itemUpdated, iz(notNullValue())) 138 | assertThat(itemUpdated!!.text, iz("Item modified")) 139 | } 140 | 141 | @Test 142 | fun should_delete_an_Item_correctly() { 143 | val item1 = createPersistenceItem(1) 144 | val item2 = createPersistenceItem(2) 145 | val item3 = createPersistenceItem(3) 146 | 147 | item1.insertOrUpdate(db) 148 | val managedItem2 = item2.insertOrUpdate(db) 149 | item3.insertOrUpdate(db) 150 | 151 | var itemsCollection = db.queryAllItemsSortedByPosition() 152 | 153 | assertThat(itemsCollection, iz(not(emptyList()))) 154 | assertThat(itemsCollection.size, iz(3)) 155 | 156 | managedItem2.delete(db) 157 | 158 | itemsCollection = db.queryAllItemsSortedByPosition() 159 | 160 | assertThat(itemsCollection, iz(not(emptyList()))) 161 | assertThat(itemsCollection.size, iz(2)) 162 | assertThat(itemsCollection.contains(item2), iz(false)) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /persistence/src/androidTest/kotlin/com/cesarvaliente/kunidirectional/persistence/handler/UpdateHandlerTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.persistence.handler 21 | 22 | import android.support.test.InstrumentationRegistry 23 | import android.support.test.runner.AndroidJUnit4 24 | import com.cesarvaliente.kunidirectional.persistence.createPersistenceItem 25 | import com.cesarvaliente.kunidirectional.persistence.insertOrUpdate 26 | import com.cesarvaliente.kunidirectional.persistence.queryAllItemsSortedByPosition 27 | import com.cesarvaliente.kunidirectional.persistence.queryByLocalId 28 | import com.cesarvaliente.kunidirectional.persistence.toPersistenceColor 29 | import com.cesarvaliente.kunidirectional.persistence.toStoreItem 30 | import com.cesarvaliente.kunidirectional.store.UpdateAction 31 | import com.cesarvaliente.kunidirectional.store.UpdateAction.ReorderItemsAction 32 | import io.realm.Realm 33 | import io.realm.RealmConfiguration 34 | import org.hamcrest.CoreMatchers.not 35 | import org.hamcrest.CoreMatchers.nullValue 36 | import org.hamcrest.MatcherAssert.assertThat 37 | import org.junit.After 38 | import org.junit.Before 39 | import org.junit.Test 40 | import org.junit.runner.RunWith 41 | import com.cesarvaliente.kunidirectional.persistence.Color as PersistenceColor 42 | import com.cesarvaliente.kunidirectional.persistence.Item as PersistenceItem 43 | import com.cesarvaliente.kunidirectional.store.Color as StoreColor 44 | import com.cesarvaliente.kunidirectional.store.Item as StoreItem 45 | import org.hamcrest.core.Is.`is` as iz 46 | 47 | @RunWith(AndroidJUnit4::class) 48 | class UpdateHandlerTest { 49 | lateinit var config: RealmConfiguration 50 | lateinit var db: Realm 51 | 52 | @Before 53 | fun setup() { 54 | Realm.init(InstrumentationRegistry.getTargetContext()) 55 | config = RealmConfiguration.Builder() 56 | .name("test.realm") 57 | .inMemory() 58 | .build() 59 | Realm.setDefaultConfiguration(config) 60 | db = Realm.getInstance(config) 61 | } 62 | 63 | @After 64 | fun clean() { 65 | db.close() 66 | Realm.deleteRealm(config) 67 | } 68 | 69 | @Test 70 | fun should_reorder_Items() { 71 | val item1 = createPersistenceItem(1) 72 | val item2 = createPersistenceItem(2) 73 | val item3 = createPersistenceItem(3) 74 | 75 | item1.insertOrUpdate(db) 76 | item2.insertOrUpdate(db) 77 | item3.insertOrUpdate(db) 78 | 79 | item1.position = 4L 80 | item2.position = 1L 81 | item3.position = 2L 82 | 83 | val listOfStoreItems = listOf(item1.toStoreItem(), item2.toStoreItem(), item3.toStoreItem()) 84 | val reorderItemsAction = ReorderItemsAction(listOfStoreItems) 85 | 86 | UpdateHandler.handle(reorderItemsAction, {}) 87 | 88 | val itemsCollection = db.queryAllItemsSortedByPosition() 89 | assertThat(itemsCollection, iz(not(nullValue()))) 90 | 91 | assertThat(itemsCollection.component1().localId, iz(item2.localId)) 92 | assertThat(itemsCollection.component2().localId, iz(item3.localId)) 93 | assertThat(itemsCollection.component3().localId, iz(item1.localId)) 94 | } 95 | 96 | @Test 97 | fun should_update_Item() { 98 | val item1 = createPersistenceItem(1) 99 | 100 | item1.insertOrUpdate(db) 101 | 102 | val NEW_TEXT = "new text" 103 | val NEW_COLOR = StoreColor.YELLOW 104 | val updateItemAction = UpdateAction.UpdateItemAction( 105 | localId = item1.localId, text = NEW_TEXT, color = NEW_COLOR) 106 | 107 | UpdateHandler.handle(updateItemAction, {}) 108 | 109 | val managedItem = db.queryByLocalId(item1.localId) 110 | assertThat(managedItem, iz(not(nullValue()))) 111 | assertThat(managedItem!!.text, iz(NEW_TEXT)) 112 | assertThat(managedItem.getColorAsEnum(), iz(NEW_COLOR.toPersistenceColor())) 113 | } 114 | 115 | @Test 116 | fun should_update_favorite_field() { 117 | val item1 = createPersistenceItem(1) 118 | 119 | item1.insertOrUpdate(db) 120 | 121 | val NEW_FAVORITE = true 122 | val updateFavoriteAction = UpdateAction.UpdateFavoriteAction( 123 | localId = item1.localId, favorite = NEW_FAVORITE) 124 | 125 | UpdateHandler.handle(updateFavoriteAction, {}) 126 | 127 | val managedItem = db.queryByLocalId(item1.localId) 128 | assertThat(managedItem, iz(not(nullValue()))) 129 | assertThat(managedItem!!.favorite, iz(NEW_FAVORITE)) 130 | } 131 | 132 | @Test 133 | fun should_update_color_field() { 134 | val item1 = createPersistenceItem(1) 135 | 136 | item1.insertOrUpdate(db) 137 | 138 | val NEW_COLOR = StoreColor.WHITE 139 | val updateColorAction = UpdateAction.UpdateColorAction( 140 | localId = item1.localId, color = NEW_COLOR) 141 | 142 | UpdateHandler.handle(updateColorAction, {}) 143 | 144 | val managedItem = db.queryByLocalId(item1.localId) 145 | assertThat(managedItem, iz(not(nullValue()))) 146 | assertThat(managedItem!!.getColorAsEnum(), iz(NEW_COLOR.toPersistenceColor())) 147 | } 148 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/edititem/EditItemActivity.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.edititem 21 | 22 | import android.content.Context 23 | import android.content.Intent 24 | import android.os.Bundle 25 | import android.view.MenuItem 26 | import com.cesarvaliente.kunidirectional.AppStore 27 | import com.cesarvaliente.kunidirectional.MainThread 28 | import com.cesarvaliente.kunidirectional.R 29 | import com.cesarvaliente.kunidirectional.ViewActivity 30 | import com.cesarvaliente.kunidirectional.changeBackgroundColor 31 | import com.cesarvaliente.kunidirectional.isNotBlank 32 | import com.cesarvaliente.kunidirectional.isNotBlankThen 33 | import com.cesarvaliente.kunidirectional.store.Color 34 | import com.cesarvaliente.kunidirectional.store.Item 35 | import com.cesarvaliente.kunidirectional.updateText 36 | import kotlinx.android.synthetic.main.edit_item_layout.* 37 | import java.lang.ref.WeakReference 38 | 39 | class EditItemActivity : ViewActivity(), EditItemViewCallback { 40 | 41 | companion object { 42 | fun createEditItemActivityIntent(context: Context): Intent = 43 | Intent(context, EditItemActivity::class.java) 44 | } 45 | 46 | override fun onCreate(savedInstanceState: Bundle?) { 47 | super.onCreate(savedInstanceState) 48 | setContentView(R.layout.edit_item_layout) 49 | setupControllerView() 50 | bindViews() 51 | } 52 | 53 | override fun setupControllerView() { 54 | val controllerView = EditItemControllerView( 55 | editItemViewCallback = WeakReference(this), 56 | store = AppStore, 57 | mainThread = MainThread(WeakReference(this))) 58 | registerControllerViewForLifecycle(controllerView) 59 | } 60 | 61 | override fun onResume() { 62 | super.onResume() 63 | controllerView.let { 64 | setDetails(it.currentItem) 65 | } 66 | } 67 | 68 | private fun bindViews() { 69 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 70 | itemRedColor.setOnClickListener { changeColor(Color.RED) } 71 | itemBlueColor.setOnClickListener { changeColor(Color.BLUE) } 72 | itemGreenColor.setOnClickListener { changeColor(Color.GREEN) } 73 | itemWhiteColor.setOnClickListener { changeColor(Color.WHITE) } 74 | itemYellowColor.setOnClickListener { changeColor(Color.YELLOW) } 75 | } 76 | 77 | private fun changeColor(color: Color) = 78 | controllerView.let { 79 | with(it.currentItem) { 80 | if (isNotEmpty() && editText.isNotBlank()) { 81 | it.updateItem(localId = localId, 82 | color = color, 83 | text = editText.text.toString()) 84 | } else { 85 | it.updateColor(localId = localId, 86 | color = color) 87 | } 88 | } 89 | } 90 | 91 | private fun setDetails(item: Item) { 92 | item.text?.let { 93 | editText.setText(it) 94 | editText.setSelection(editText.length()) 95 | } 96 | when (item.color) { 97 | Color.RED -> itemRedColor.isChecked = true 98 | Color.BLUE -> itemBlueColor.isChecked = true 99 | Color.GREEN -> itemGreenColor.isChecked = true 100 | Color.WHITE -> itemWhiteColor.isChecked = true 101 | Color.YELLOW -> itemYellowColor.isChecked = true 102 | } 103 | } 104 | 105 | private fun createOrUpdateItem(item: Item) = 106 | editText.isNotBlankThen(blockTextNotBlank = { 107 | if (item.isEmpty()) createItem(item.localId) 108 | else updateItem(item.localId) 109 | }) 110 | 111 | private fun createItem(localId: String) = 112 | controllerView.let { 113 | with(it.currentItem) { 114 | it.createItem( 115 | localId = localId, 116 | text = editText.text.toString(), 117 | favorite = favorite, 118 | color = color, 119 | position = position) 120 | } 121 | } 122 | 123 | private fun updateItem(localId: String) = 124 | controllerView.let { 125 | it.updateItem( 126 | localId = localId, 127 | text = editText.text.toString(), 128 | color = it.currentItem.color) 129 | } 130 | 131 | override fun onBackPressed() { 132 | backAndUpdate() 133 | } 134 | 135 | private fun backAndUpdate() { 136 | createOrUpdateItem(controllerView.currentItem) 137 | controllerView.backToItems() 138 | } 139 | 140 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 141 | if (item?.itemId == android.R.id.home) { 142 | backAndUpdate() 143 | return true 144 | } 145 | return super.onOptionsItemSelected(item) 146 | } 147 | 148 | override fun updateItem(item: Item) { 149 | editText.updateText(item.text) 150 | itemLayout.changeBackgroundColor(item.color) 151 | } 152 | 153 | override fun backToItemsList() = 154 | finish() 155 | } 156 | 157 | interface EditItemViewCallback { 158 | fun updateItem(item: Item) 159 | fun backToItemsList() 160 | } 161 | -------------------------------------------------------------------------------- /app/src/test/kotlin/com/cesarvaliente/kunidirectional/itemslist/ItemsControllerViewTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.itemslist 21 | 22 | import com.cesarvaliente.kunidirectional.TestStore 23 | import com.cesarvaliente.kunidirectional.createItem 24 | import com.cesarvaliente.kunidirectional.store.ItemsListScreen 25 | import com.cesarvaliente.kunidirectional.store.Navigation 26 | import com.cesarvaliente.kunidirectional.store.State 27 | import com.nhaarman.mockito_kotlin.argumentCaptor 28 | import com.nhaarman.mockito_kotlin.spy 29 | import com.nhaarman.mockito_kotlin.times 30 | import com.nhaarman.mockito_kotlin.verify 31 | import org.hamcrest.CoreMatchers.* 32 | import org.junit.After 33 | import org.junit.Assert.assertThat 34 | import org.junit.Before 35 | import org.junit.Test 36 | import org.mockito.Mock 37 | import org.mockito.MockitoAnnotations 38 | import java.lang.ref.WeakReference 39 | import org.hamcrest.CoreMatchers.`is` as iz 40 | 41 | class ItemsControllerViewTest { 42 | private @Mock lateinit var itemsViewCallback: ItemsViewCallback 43 | private lateinit var itemsControllerView: ItemsControllerView 44 | private lateinit var itemsControllerViewSpy: ItemsControllerView 45 | private lateinit var store: TestStore 46 | 47 | @Before 48 | fun setup() { 49 | MockitoAnnotations.initMocks(this) 50 | 51 | store = TestStore 52 | itemsControllerView = ItemsControllerView( 53 | itemsViewCallback = WeakReference(itemsViewCallback), 54 | store = store) 55 | 56 | itemsControllerView.isActivityRunning = true 57 | itemsControllerViewSpy = spy(itemsControllerView) 58 | store.stateHandlers.add(itemsControllerViewSpy) 59 | } 60 | 61 | @After 62 | fun clean() { 63 | store.clear() 64 | } 65 | 66 | @Test 67 | fun should_fetch_Items_and_handle_State() { 68 | val item1 = createItem(1) 69 | val item2 = createItem(2) 70 | val item3 = createItem(3) 71 | val listOfItems = listOf(item1, item2, item3) 72 | 73 | val state = State(ItemsListScreen(items = listOfItems)) 74 | store.dispatch(state) 75 | 76 | itemsControllerViewSpy.fetchItems() 77 | 78 | argumentCaptor().apply { 79 | verify(itemsControllerViewSpy, times(2)).handleState(capture()) 80 | 81 | with(lastValue.itemsListScreen.items) { 82 | assertThat(this, iz(not(emptyList()))) 83 | assertThat(this.size, iz(listOfItems.size)) 84 | assertThat(this, iz(listOfItems)) 85 | } 86 | 87 | assertThat(lastValue.editItemScreen.currentItem.isEmpty(), iz(true)) 88 | assertThat(lastValue.navigation, iz(Navigation.ITEMS_LIST)) 89 | } 90 | } 91 | 92 | @Test 93 | fun should_edit_Item_and_handle_State() { 94 | val item1 = createItem(1) 95 | val state = State(itemsListScreen = ItemsListScreen(listOf(item1)), 96 | navigation = Navigation.ITEMS_LIST) 97 | store.dispatch(state) 98 | 99 | itemsControllerViewSpy.toEditItemScreen(item1) 100 | argumentCaptor().apply { 101 | verify(itemsControllerViewSpy, times(2)).handleState(capture()) 102 | 103 | assertThat(lastValue.editItemScreen.currentItem, iz(item1)) 104 | assertThat(lastValue.navigation, iz(Navigation.EDIT_ITEM)) 105 | } 106 | } 107 | 108 | @Test 109 | fun should_reorder_Items_and_handle_State() { 110 | val item1 = createItem(1) 111 | val item2 = createItem(2) 112 | val item3 = createItem(3) 113 | val defaultList = listOf(item3, item2, item1) 114 | 115 | val state = State(itemsListScreen = ItemsListScreen(defaultList), 116 | navigation = Navigation.ITEMS_LIST) 117 | store.dispatch(state) 118 | 119 | val reorderedList = listOf(item1, item2, item3) 120 | itemsControllerViewSpy.reorderItems(reorderedList) 121 | 122 | argumentCaptor().apply { 123 | verify(itemsControllerViewSpy, times(2)).handleState(capture()) 124 | 125 | with(lastValue.itemsListScreen) { 126 | assertThat(items.size, iz(defaultList.size)) 127 | assertThat(items, iz(not(defaultList))) 128 | assertThat(items, iz(reorderedList)) 129 | } 130 | assertThat(lastValue.navigation, iz(Navigation.ITEMS_LIST)) 131 | } 132 | } 133 | 134 | @Test 135 | fun should_change_Item_favorite_status_and_handle_state() { 136 | val item1 = createItem(1) 137 | val state = State(itemsListScreen = ItemsListScreen(listOf(item1)), 138 | navigation = Navigation.ITEMS_LIST) 139 | store.dispatch(state) 140 | 141 | itemsControllerViewSpy.changeFavoriteStatus(item1) 142 | argumentCaptor().apply { 143 | verify(itemsControllerViewSpy, times(2)).handleState(capture()) 144 | 145 | with(lastValue.itemsListScreen.items) { 146 | assertThat(size, iz(1)) 147 | assertThat(component1().favorite, iz(not(item1.favorite))) 148 | } 149 | assertThat(lastValue.editItemScreen.currentItem.isEmpty(), iz(true)) 150 | assertThat(lastValue.navigation, iz(Navigation.ITEMS_LIST)) 151 | } 152 | } 153 | 154 | @Test 155 | fun should_delete_Item_and_handle_State() { 156 | val item1 = createItem(1) 157 | val item2 = createItem(2) 158 | val item3 = createItem(3) 159 | val defaultList = listOf(item1, item2, item3) 160 | 161 | val state = State(itemsListScreen = ItemsListScreen(defaultList), 162 | navigation = Navigation.ITEMS_LIST) 163 | store.dispatch(state) 164 | 165 | itemsControllerViewSpy.deleteItem(item1) 166 | 167 | argumentCaptor().apply { 168 | verify(itemsControllerViewSpy, times(2)).handleState(capture()) 169 | 170 | with(lastValue.itemsListScreen) { 171 | assertThat(items.size, iz(not(defaultList.size))) 172 | assertThat(items.size, iz(2)) 173 | assertThat(items, hasItem(not(item1))) 174 | assertThat(items.component1(), iz(item2)) 175 | assertThat(items.component2(), iz(item3)) 176 | } 177 | assertThat(lastValue.navigation, iz(Navigation.ITEMS_LIST)) 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/cesarvaliente/kunidirectional/itemslist/ItemsActivity.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.itemslist 21 | 22 | import android.annotation.SuppressLint 23 | import android.annotation.TargetApi 24 | import android.os.Build 25 | import android.os.Bundle 26 | import android.os.Handler 27 | import android.preference.PreferenceManager 28 | import android.support.design.widget.Snackbar 29 | import android.support.v4.content.ContextCompat 30 | import android.support.v7.widget.LinearLayoutManager 31 | import android.support.v7.widget.helper.ItemTouchHelper 32 | import android.view.Menu 33 | import android.view.MenuItem 34 | import com.cesarvaliente.kunidirectional.AppStore 35 | import com.cesarvaliente.kunidirectional.MainThread 36 | import com.cesarvaliente.kunidirectional.R 37 | import com.cesarvaliente.kunidirectional.ViewActivity 38 | import com.cesarvaliente.kunidirectional.edititem.EditItemActivity 39 | import com.cesarvaliente.kunidirectional.itemslist.recyclerview.ItemTouchHelperCallback 40 | import com.cesarvaliente.kunidirectional.itemslist.recyclerview.ItemsAdapter 41 | import com.cesarvaliente.kunidirectional.store.Item 42 | import kotlinx.android.synthetic.main.items_layout.* 43 | import org.jetbrains.anko.doFromSdk 44 | import org.jetbrains.anko.toast 45 | import java.lang.ref.WeakReference 46 | 47 | class ItemsActivity : ViewActivity(), ItemsViewCallback { 48 | private val itemsAdapter: ItemsAdapter 49 | 50 | private val isPersistenceEnabled: Boolean 51 | get () { 52 | println("called") 53 | return PreferenceManager.getDefaultSharedPreferences(this).getBoolean( 54 | getString(R.string.pref_persistence_key), true) 55 | } 56 | 57 | init { 58 | itemsAdapter = ItemsAdapter( 59 | items = emptyList(), 60 | itemClick = { item -> openEditItemScreen(item) }, 61 | setFavorite = { item -> changeFavoriteStatus(item) }, 62 | updateItemsPositions = { items -> reorderItems(items) }, 63 | deleteItem = { item -> deleteItem(item) } 64 | ) 65 | } 66 | 67 | private val handler = Handler() 68 | 69 | override fun onCreate(savedInstanceState: Bundle?) { 70 | super.onCreate(savedInstanceState) 71 | setContentView(R.layout.items_layout) 72 | setupControllerView() 73 | bindViews() 74 | } 75 | 76 | override fun setupControllerView() { 77 | val controllerView = ItemsControllerView( 78 | itemsViewCallback = WeakReference(this), 79 | store = AppStore, 80 | mainThread = MainThread(WeakReference(this))) 81 | registerControllerViewForLifecycle(controllerView) 82 | } 83 | 84 | override fun onResume() { 85 | super.onResume() 86 | controllerView.fetchItems() 87 | } 88 | 89 | @SuppressLint("NewApi") 90 | private fun bindViews() { 91 | setContentView(R.layout.items_layout) 92 | setStatusBarColor() 93 | setupRecyclerView() 94 | newItem.setOnClickListener { openEditItemScreen(controllerView.currentItem) } 95 | } 96 | 97 | private fun setupRecyclerView() { 98 | val linearLayoutManager = LinearLayoutManager(this) 99 | itemsRecyclerView.layoutManager = linearLayoutManager 100 | itemsRecyclerView.adapter = itemsAdapter 101 | 102 | val touchHelperCallback = ItemTouchHelperCallback(itemsAdapter) 103 | val touchHelper = ItemTouchHelper(touchHelperCallback) 104 | touchHelper.attachToRecyclerView(itemsRecyclerView) 105 | } 106 | 107 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 108 | private fun setStatusBarColor() = 109 | doFromSdk(Build.VERSION_CODES.LOLLIPOP) { 110 | window.statusBarColor = ContextCompat.getColor(this, R.color.colorPrimaryDark) 111 | } 112 | 113 | private fun openEditItemScreen(item: Item) = 114 | controllerView.toEditItemScreen(item) 115 | 116 | private fun reorderItems(items: List) = 117 | controllerView.reorderItems(items) 118 | 119 | private fun changeFavoriteStatus(item: Item) = 120 | controllerView.changeFavoriteStatus(item) 121 | 122 | private fun deleteItem(item: Item) { 123 | val TIME_TO_WAIT = 2000 124 | val deleteItemRunnable = Runnable { controllerView.deleteItem(item) } 125 | Snackbar.make(itemsCoordinatorLayout, R.string.item_deleted, TIME_TO_WAIT) 126 | .setAction(R.string.item_deleted_undo, 127 | { 128 | handler.removeCallbacksAndMessages(null) 129 | with(controllerView.state.itemsListScreen) { 130 | updateItems(items) 131 | } 132 | }) 133 | .addCallback(object : Snackbar.Callback() { 134 | override fun onShown(snackbar: Snackbar?) { 135 | super.onShown(snackbar) 136 | handler.postDelayed(deleteItemRunnable, TIME_TO_WAIT.toLong()) 137 | } 138 | }) 139 | .show() 140 | } 141 | 142 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 143 | menuInflater.inflate(R.menu.items_menu, menu) 144 | menu.findItem(R.id.item_toggle_persistence)?.isChecked = isPersistenceEnabled 145 | return true 146 | } 147 | 148 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 149 | when (item.itemId) { 150 | R.id.item_toggle_persistence -> togglePersistenceOptionMenu() 151 | else -> return super.onOptionsItemSelected(item) 152 | } 153 | return true 154 | } 155 | 156 | override fun onPrepareOptionsMenu(menu: Menu?): Boolean { 157 | val menuItem = menu?.findItem(R.id.item_toggle_persistence) 158 | menuItem?.isChecked = isPersistenceEnabled 159 | return super.onPrepareOptionsMenu(menu) 160 | } 161 | 162 | private fun togglePersistenceOptionMenu() { 163 | val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) 164 | val prefPersistenceKey = getString(R.string.pref_persistence_key) 165 | val isEnabled = sharedPrefs.getBoolean(prefPersistenceKey, true) 166 | sharedPrefs.edit().putBoolean(prefPersistenceKey, !isEnabled).apply() 167 | 168 | toast(R.string.persistence_changed) 169 | } 170 | 171 | override fun updateItems(items: List) = 172 | itemsAdapter.updateItems(items) 173 | 174 | override fun goToEditItem() { 175 | val intent = EditItemActivity.createEditItemActivityIntent(this) 176 | startActivity(intent) 177 | } 178 | } 179 | 180 | interface ItemsViewCallback { 181 | fun updateItems(items: List) 182 | fun goToEditItem() 183 | } 184 | -------------------------------------------------------------------------------- /app/src/test/kotlin/com/cesarvaliente/kunidirectional/edititem/EditItemControllerViewTest.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Cesar Valiente & Corey Shaw 3 | * 4 | * https://github.com/CesarValiente 5 | * https://github.com/coshaw 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.cesarvaliente.kunidirectional.edititem 21 | 22 | import com.cesarvaliente.kunidirectional.COLOR 23 | import com.cesarvaliente.kunidirectional.FAVORITE 24 | import com.cesarvaliente.kunidirectional.LOCAL_ID 25 | import com.cesarvaliente.kunidirectional.POSITION 26 | import com.cesarvaliente.kunidirectional.TEXT 27 | import com.cesarvaliente.kunidirectional.TestStore 28 | import com.cesarvaliente.kunidirectional.createItem 29 | import com.cesarvaliente.kunidirectional.store.Color 30 | import com.cesarvaliente.kunidirectional.store.EditItemScreen 31 | import com.cesarvaliente.kunidirectional.store.Item 32 | import com.cesarvaliente.kunidirectional.store.ItemsListScreen 33 | import com.cesarvaliente.kunidirectional.store.Navigation 34 | import com.cesarvaliente.kunidirectional.store.State 35 | import com.nhaarman.mockito_kotlin.any 36 | import com.nhaarman.mockito_kotlin.argumentCaptor 37 | import com.nhaarman.mockito_kotlin.reset 38 | import com.nhaarman.mockito_kotlin.spy 39 | import com.nhaarman.mockito_kotlin.times 40 | import com.nhaarman.mockito_kotlin.verify 41 | import org.hamcrest.CoreMatchers.not 42 | import org.junit.After 43 | import org.junit.Assert.assertThat 44 | import org.junit.Before 45 | import org.junit.Test 46 | import org.mockito.Mock 47 | import org.mockito.MockitoAnnotations 48 | import java.lang.ref.WeakReference 49 | import org.hamcrest.CoreMatchers.`is` as iz 50 | 51 | class EditItemControllerViewTest { 52 | private @Mock lateinit var editItemViewCallback: EditItemViewCallback 53 | private lateinit var editItemControllerView: EditItemControllerView 54 | private lateinit var editItemControllerViewSpy: EditItemControllerView 55 | private lateinit var store: TestStore 56 | 57 | @Before 58 | fun setup() { 59 | MockitoAnnotations.initMocks(this) 60 | 61 | store = TestStore 62 | editItemControllerView = EditItemControllerView( 63 | editItemViewCallback = WeakReference(editItemViewCallback), 64 | store = store) 65 | 66 | editItemControllerView.isActivityRunning = true 67 | editItemControllerViewSpy = spy(editItemControllerView) 68 | store.stateHandlers.add(editItemControllerViewSpy) 69 | 70 | } 71 | 72 | @After 73 | fun clean() { 74 | store.clear() 75 | } 76 | 77 | @Test 78 | fun should_create_an_item_and_handle_State() { 79 | editItemControllerViewSpy.createItem(localId = LOCAL_ID, text = TEXT, 80 | favorite = FAVORITE, color = COLOR, position = POSITION) 81 | argumentCaptor().apply { 82 | verify(editItemControllerViewSpy).handleState(capture()) 83 | 84 | with(lastValue.itemsListScreen.items) { 85 | assertThat(this, iz(not(emptyList()))) 86 | assertThat(this.size, iz(1)) 87 | 88 | with(component1()) { 89 | assertThat(this.localId, iz(LOCAL_ID)) 90 | assertThat(this.text, iz(TEXT)) 91 | assertThat(this.favorite, iz(FAVORITE)) 92 | assertThat(this.color, iz(COLOR)) 93 | assertThat(this.position, iz(POSITION)) 94 | } 95 | } 96 | 97 | assertThat(lastValue.editItemScreen.currentItem.isEmpty(), iz(true)) 98 | assertThat(lastValue.navigation, iz(Navigation.ITEMS_LIST)) 99 | } 100 | } 101 | 102 | @Test 103 | fun should_update_an_item_and_handle_State() { 104 | val item1 = createItem(1) 105 | val state = State(itemsListScreen = ItemsListScreen(listOf(item1)), 106 | editItemScreen = EditItemScreen(item1), 107 | navigation = Navigation.EDIT_ITEM) 108 | store.dispatch(state) 109 | 110 | val NEW_TEXT = "new text" 111 | editItemControllerViewSpy.updateItem(localId = item1.localId, 112 | text = NEW_TEXT, color = Color.GREEN) 113 | 114 | argumentCaptor().apply { 115 | verify(editItemControllerViewSpy, times(2)).handleState(capture()) 116 | 117 | assertThat(lastValue.itemsListScreen.items, iz(not(emptyList()))) 118 | assertThat(lastValue.itemsListScreen.items.size, iz(1)) 119 | 120 | fun verifyItem(item: Item) = 121 | with(item) { 122 | assertThat(localId, iz(item1.localId)) 123 | assertThat(text, iz(NEW_TEXT)) 124 | assertThat(favorite, iz(item1.favorite)) 125 | assertThat(color, iz(Color.GREEN)) 126 | assertThat(position, iz(item1.position)) 127 | } 128 | verifyItem(lastValue.itemsListScreen.items.component1()) 129 | verifyItem(lastValue.editItemScreen.currentItem) 130 | assertThat(lastValue.navigation, iz(Navigation.EDIT_ITEM)) 131 | } 132 | } 133 | 134 | @Test 135 | fun should_update_Item_color_and_handle_State() { 136 | val item1 = createItem(1) 137 | val state = State(itemsListScreen = ItemsListScreen(listOf(item1)), 138 | editItemScreen = EditItemScreen(item1), 139 | navigation = Navigation.EDIT_ITEM) 140 | store.dispatch(state) 141 | 142 | editItemControllerViewSpy.updateColor(localId = item1.localId, 143 | color = Color.BLUE) 144 | 145 | argumentCaptor().apply { 146 | verify(editItemControllerViewSpy, times(2)).handleState(capture()) 147 | 148 | assertThat(lastValue.itemsListScreen.items, iz(not(emptyList()))) 149 | assertThat(lastValue.itemsListScreen.items.size, iz(1)) 150 | 151 | fun verifyItem(item: Item) = 152 | with(item) { 153 | assertThat(localId, iz(item1.localId)) 154 | assertThat(text, iz(item1.text)) 155 | assertThat(favorite, iz(item1.favorite)) 156 | assertThat(color, iz(Color.BLUE)) 157 | assertThat(position, iz(item1.position)) 158 | } 159 | verifyItem(lastValue.itemsListScreen.items.component1()) 160 | verifyItem(lastValue.editItemScreen.currentItem) 161 | assertThat(lastValue.navigation, iz(Navigation.EDIT_ITEM)) 162 | } 163 | } 164 | 165 | @Test 166 | fun should_back_to_Items_list_and_handle_State() { 167 | val state = State(navigation = Navigation.EDIT_ITEM) 168 | store.dispatch(state) 169 | 170 | editItemControllerViewSpy.backToItems() 171 | 172 | argumentCaptor().apply { 173 | verify(editItemControllerViewSpy, times(2)).handleState(capture()) 174 | 175 | assertThat(lastValue.itemsListScreen.items, iz(emptyList())) 176 | assertThat(lastValue.editItemScreen.currentItem.isEmpty(), iz(true)) 177 | assertThat(lastValue.navigation, iz(Navigation.ITEMS_LIST)) 178 | } 179 | } 180 | 181 | @Test 182 | fun should_handle_State_and_call_updateItem_function() { 183 | val state = State(navigation = Navigation.EDIT_ITEM) 184 | editItemControllerView.handleState(state) 185 | verify(editItemViewCallback).updateItem(any()) 186 | } 187 | 188 | @Test 189 | fun should_handle_State_and_call_backToItemsList_function() { 190 | 191 | val state = State(navigation = Navigation.ITEMS_LIST) 192 | reset(editItemViewCallback) 193 | editItemControllerView.handleState(state) 194 | verify(editItemViewCallback).backToItemsList() 195 | } 196 | } --------------------------------------------------------------------------------