├── 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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------