Skip to content

Commit 545cbdf

Browse files
author
Mohanraj K.M
committedNov 10, 2017
Add updated func
1 parent f913d52 commit 545cbdf

17 files changed

+1415
-0
lines changed
 

‎.gitignore

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
gradlew*
2+
*.iml
3+
.gradle
4+
/local.properties
5+
/.idea/workspace.xml
6+
/.idea/libraries
7+
.DS_Store
8+
/build
9+
/captures
10+
.externalNativeBuild
11+
build/
12+
projectFilesBackup/
13+
gradle/
14+
.gradle/
15+
.idea/**
16+
.notidea/
17+
*.DS_Store
18+
*.iml
19+
app/build/**
20+
21+
22+
# built application files
23+
*.apk
24+
*.ap_
25+
26+
# files for the dex VM
27+
*.dex
28+
29+
# Java class files
30+
*.class
31+
32+
# built native files (uncomment if you build your own)
33+
# *.o
34+
# *.so
35+
36+
# generated files
37+
bin/
38+
gen/
39+
40+
# Ignore gradle files
41+
.gradle/
42+
build/
43+
44+
# Local configuration file (sdk path, etc)
45+
local.properties
46+
47+
# Proguard folder generated by Eclipse
48+
proguard/
49+
50+
# Eclipse Metadata
51+
.metadata/
52+
53+
# Mac OS X clutter
54+
*.DS_Store
55+
56+
# Windows clutter
57+
Thumbs.db
58+
# Intellij IDEA (see https://intellij-support.jetbrains.com/entries/23393067)
59+
.idea/workspace.xml
60+
.idea/tasks.xml
61+
.idea/datasources.xml
62+
.idea/dataSources.ids

‎build.gradle

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Top-level build file where you can add configuration options common to all sub-projects/modules.
2+
group 'org.rekotlinrouter'
3+
version '0.1.0-SNAPSHOT'
4+
5+
buildscript {
6+
ext.kotlin_version = '1.1.50'
7+
ext.kotlin_version = '1.1.3-2'
8+
ext.dokka_version = '0.9.15'
9+
ext.rekotlin_version = '1.1.1'
10+
ext.junitPlugin_version = '1.0.0'
11+
ext.junit_jupiter_version = '5.0.0'
12+
repositories {
13+
google()
14+
jcenter()
15+
mavenLocal()
16+
}
17+
dependencies {
18+
classpath 'com.android.tools.build:gradle:3.0.0-alpha4'
19+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
20+
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:${dokka_version}"
21+
}
22+
}
23+
24+
25+
allprojects {
26+
repositories {
27+
google()
28+
jcenter()
29+
mavenCentral()
30+
}
31+
}
32+
33+
task clean(type: Delete) {
34+
delete rootProject.buildDir
35+
}
36+

‎rekrouter/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

‎rekrouter/build.gradle

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
apply plugin: 'com.android.library'
2+
apply plugin: 'kotlin-android'
3+
apply plugin: 'kotlin-android-extensions'
4+
//apply plugin: 'jacoco'
5+
apply plugin: 'maven-publish'
6+
//apply plugin: 'org.jetbrains.dokka'
7+
apply plugin: 'org.jetbrains.dokka-android'
8+
9+
10+
ext.junitPlugin_version = '1.0.0'
11+
ext.junit_jupiter_version = '5.0.0'
12+
android {
13+
compileSdkVersion 25
14+
buildToolsVersion "26.0.0"
15+
16+
17+
defaultConfig {
18+
minSdkVersion 16
19+
targetSdkVersion 25
20+
versionCode 1
21+
versionName "1.0"
22+
23+
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
24+
25+
}
26+
buildTypes {
27+
debug {
28+
//TODO: Keep it false until..
29+
// https://stackoverflow.com/questions/39195754/java-lang-noclassdeffounderror-failed-resolution-of-lorg-jacoco-agent-rt-inter
30+
testCoverageEnabled = false
31+
}
32+
release {
33+
minifyEnabled false
34+
testCoverageEnabled = false
35+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
36+
}
37+
}
38+
39+
testOptions {
40+
unitTests.all {
41+
// jacoco {
42+
// includeNoLocationClasses = true
43+
// }
44+
}
45+
}
46+
}
47+
48+
//jacoco {
49+
// toolVersion = "0.7.6.201602180812"
50+
// reportsDir = file("$buildDir/customJacocoReportDir")
51+
//}
52+
//
53+
//task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) {
54+
//
55+
// reports {
56+
// xml.enabled = true
57+
// html.enabled = true
58+
// }
59+
//
60+
// def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*']
61+
// def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug", excludes: fileFilter)
62+
// def kotlinDebugTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/debug", excludes: fileFilter)
63+
// def mainSrc = "${project.projectDir}/src/androidTest/java"
64+
//
65+
// sourceDirectories = files([mainSrc])
66+
// classDirectories = files([debugTree], [kotlinDebugTree])
67+
// executionData = fileTree(dir: "$buildDir", includes: [
68+
// "jacoco/testDebugUnitTest.exec",
69+
// "outputs/code-coverage/connected/*coverage.ec"
70+
// ])
71+
//}
72+
73+
74+
task sourcesJar(type: Jar) {
75+
from android.sourceSets.main.java.srcDirs
76+
classifier = 'sources'
77+
}
78+
79+
//task copyTestClasses(type: Copy) {
80+
// from "${buildDir}/tmp/kotlin-classes/debugUnitTest"
81+
// into "${buildDir}/intermediates/classes/debug"
82+
//}
83+
84+
dependencies {
85+
implementation fileTree(include: ['*.jar'], dir: 'libs')
86+
87+
androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', {
88+
exclude group: 'com.android.support', module: 'support-annotations'
89+
})
90+
implementation 'com.android.support:appcompat-v7:25.4.0'
91+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
92+
//implementation files('/Users/mkaratadipalayam/mohan/technotes/client/apps/android/kotlin/rekotlin/myFork/ReKotlin/build/libs/rekotlin-0.1.0-SNAPSHOT.jar')
93+
// TODO: Fix the below mess - compileOnly & testImplementation of rekotlin
94+
compileOnly files('/Users/mkaratadipalayam/mohan/technotes/client/apps/android/kotlin/rekotlin/myFork/ReKotlin/build/libs/rekotlin-0.1.0-SNAPSHOT.jar')
95+
testImplementation files('/Users/mkaratadipalayam/mohan/technotes/client/apps/android/kotlin/rekotlin/myFork/ReKotlin/build/libs/rekotlin-0.1.0-SNAPSHOT.jar')
96+
testImplementation 'junit:junit:4.12'
97+
testImplementation 'org.mockito:mockito-core:1.10.19'
98+
testImplementation 'org.assertj:assertj-core:3.6.2'
99+
testImplementation 'org.powermock:powermock-module-junit4:1.7.0RC2', 'org.powermock:powermock-api-mockito2:1.7.0RC2'
100+
testImplementation group: 'com.googlecode.junit-toolbox', name: 'junit-toolbox', version: '1.10'
101+
testImplementation 'org.awaitility:awaitility:3.0.0'
102+
}
103+
104+
105+
sourceSets {
106+
main.java.srcDirs += 'src/main/java'
107+
}
108+
109+
task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
110+
outputFormat = 'javadoc'
111+
outputDirectory = "$buildDir/javadoc"
112+
inputs.dir 'src/main/java'
113+
}
114+
task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
115+
classifier = 'javadoc'
116+
from "$buildDir/javadoc"
117+
}
118+
119+
artifacts {
120+
// archives aar
121+
archives sourcesJar
122+
archives javadocJar
123+
}
124+
125+
//publishing {
126+
// publications {
127+
// maven(MavenPublication) {
128+
// groupId 'org.rekotlinrouter'
129+
// artifactId 'rekotlinrouter'
130+
// from components.java
131+
//
132+
// artifact(javadocJar){
133+
// classifier = 'javadoc'
134+
// }
135+
//
136+
// artifact(sourcesJar){
137+
// classifier = 'sources'
138+
// }
139+
//
140+
// repositories {
141+
//
142+
// /*
143+
// maven {
144+
// url 's3://rekotlin/snapshots'
145+
// credentials(AwsCredentials) {
146+
// accessKey AWS_ACCESS_KEY // put this in gradle.properties
147+
// secretKey AWS_SECRET_KEY // put this in gradle.properties
148+
// }
149+
// }*/
150+
//
151+
// maven {
152+
// url "file:/${project.projectDir}/artifacts"
153+
// }
154+
// }
155+
// }
156+
// }
157+
//}
158+
159+
//dokka {
160+
// outputFormat = 'html'
161+
// outputDirectory = "$buildDir/javadoc"
162+
//}

‎rekrouter/proguard-rules.pro

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Add project specific ProGuard rules here.
2+
# By default, the flags in this file are appended to flags specified
3+
# in /Users/mkaratadipalayam/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
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="org.rekotlinrouter.rekrouter" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.rekotlinrouter
2+
3+
import tw.geothings.rekotlin.Action
4+
5+
6+
class SetRouteAction(var route: Route,
7+
var animated: Boolean = true,
8+
action: StandardAction? = null): StandardActionConvertible {
9+
10+
companion object {
11+
const val type = "RE_KOTLIN_ROUTER_SET_ROUTE"
12+
}
13+
14+
15+
init {
16+
// TODO: Convert the below to ArrayList
17+
if (action != null) {
18+
19+
route = action.payload?.keys?.toTypedArray() as Route
20+
animated = action.payload["animated"] as Boolean
21+
}
22+
}
23+
24+
override fun toStandardAction(): StandardAction {
25+
26+
val payloadMap: HashMap<String,Any> = HashMap()
27+
payloadMap.put("route",this.route)
28+
payloadMap.put("animated",this.animated)
29+
return StandardAction(type = SetRouteAction.type,
30+
payload = payloadMap,
31+
isTypedAction = true)
32+
}
33+
34+
35+
36+
}
37+
38+
class SetRouteSpecificData ( val route: Route, val data: Any): Action
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.rekotlinrouter
2+
3+
import tw.geothings.rekotlin.Action
4+
5+
6+
7+
/**
8+
The Navigation Reducer handles the state slice concerned with storing the current navigation
9+
information. Note, that this reducer is **not** a *top-level* reducer, you need to use it within
10+
another reducer and pass in the relevant state slice. Take a look at the testcases to see an
11+
example set up.
12+
*/
13+
class NavigationReducer {
14+
15+
companion object NavRed {
16+
17+
fun handleAction(action: Action, state: NavigationState?): NavigationState {
18+
var navigationState = state ?: NavigationState()
19+
20+
when(action) {
21+
is SetRouteAction -> navigationState = setRoute(navigationState,action)
22+
is SetRouteSpecificData -> navigationState = setRouteSpecificData(navigationState, action.route, action.data)
23+
else -> navigationState = NavigationState()
24+
}
25+
return navigationState
26+
}
27+
28+
fun setRoute(state: NavigationState, setRouteAction: SetRouteAction): NavigationState {
29+
val navigationState = state
30+
31+
navigationState.route = setRouteAction.route
32+
navigationState.changeRouteAnimated = setRouteAction.animated
33+
34+
return navigationState
35+
}
36+
37+
fun setRouteSpecificData(state: NavigationState, route: Route, data: Any): NavigationState {
38+
val routeString = FullRoute(route).routeString
39+
40+
if (state.routeSpecificState.containsKey(routeString)) {
41+
state.routeSpecificState.replace(routeString, data)
42+
} else {
43+
state.routeSpecificState.put(routeString, data)
44+
}
45+
return state
46+
}
47+
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.rekotlinrouter
2+
3+
4+
5+
class FullRoute(route: Route) {
6+
val routeString: String
7+
8+
init {
9+
this.routeString = route.joinToString(separator = "/")
10+
}
11+
12+
}
13+
14+
data class NavigationState(var route: Route = arrayListOf() ,
15+
var routeSpecificState: HashMap<String,Any> = HashMap() ,
16+
var changeRouteAnimated: Boolean = true) {
17+
18+
19+
fun <T> getRouteSpecificState(givenRoutes: Route): T? {
20+
val fullroute = FullRoute(givenRoutes)
21+
val routeString = fullroute.routeString
22+
23+
return routeSpecificState[routeString] as? T
24+
}
25+
}
26+
27+
28+
29+
interface HasNavigationState {
30+
var navigationState: NavigationState
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.rekotlinrouter
2+
3+
typealias RoutingCompletionHandler = () -> Unit
4+
5+
typealias RouteElementIdentifier = String
6+
typealias Route = ArrayList<RouteElementIdentifier>
7+
8+
9+
interface Routable {
10+
11+
fun pushRouteSegment(routeElementIdentifier: RouteElementIdentifier,
12+
animated: Boolean,
13+
completionHandler: RoutingCompletionHandler): Routable
14+
15+
fun popRouteSegment(routeElementIdentifier: RouteElementIdentifier,
16+
animated: Boolean,
17+
completionHandler: RoutingCompletionHandler)
18+
19+
fun changeRouteSegment(from: RouteElementIdentifier,
20+
to: RouteElementIdentifier,
21+
animated: Boolean,
22+
completionHandler: RoutingCompletionHandler): Routable
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package org.rekotlinrouter
2+
3+
import android.os.Handler
4+
import android.os.Looper
5+
import tw.geothings.rekotlin.StateType
6+
import tw.geothings.rekotlin.Store
7+
import tw.geothings.rekotlin.StoreSubscriber
8+
import tw.geothings.rekotlin.Subscription
9+
10+
11+
// TODO: Check is this need to be a singleton ?
12+
sealed class RoutingAction
13+
data class push(val responsibleRoutableIndex: Int, val segmentToBePushed: RouteElementIdentifier): RoutingAction()
14+
data class pop(val responsibleRoutableIndex: Int, val segmentToBePopped: RouteElementIdentifier): RoutingAction()
15+
data class change(val responsibleRoutableIndex: Int, val segmentToBeReplaced: RouteElementIdentifier,
16+
val newSegment: RouteElementIdentifier): RoutingAction()
17+
18+
19+
class Router<routerStateType: StateType> (var store: Store<routerStateType>,
20+
rootRoutable: Routable,
21+
stateTransform: (Subscription<routerStateType>) -> Subscription<NavigationState>): StoreSubscriber<NavigationState> {
22+
23+
24+
var lastNavigationState = NavigationState()
25+
// TODO: Collections.synchronizedList vs CopyOnWriteArrayList
26+
// var routables: List<Routable> = Collections.synchronizedList(arrayListOf<Routable>())
27+
var routables: ArrayList<Routable> = arrayListOf()
28+
29+
30+
init {
31+
this.routables.add(rootRoutable)
32+
this.store.subscribe(this,stateTransform)
33+
}
34+
35+
private val mainThreadHandler = Handler(Looper.getMainLooper())
36+
37+
override fun newState(state: NavigationState) {
38+
val routingActions = routingActionsForTransitionFrom(lastNavigationState.route,state.route)
39+
if (routingActions.size > 0) {
40+
routingActions.forEach { routingAction ->
41+
routingSerailActionHandler(routingAction, state)
42+
}
43+
lastNavigationState = state.copy()
44+
}
45+
}
46+
47+
48+
49+
50+
private fun routingSerailActionHandler(routingAction: RoutingAction, state: NavigationState) {
51+
52+
synchronized(lock = routables){
53+
when(routingAction) {
54+
55+
is pop -> {
56+
mainThreadHandler.post {
57+
this.routables[routingAction.responsibleRoutableIndex]
58+
.popRouteSegment(routeElementIdentifier = routingAction.segmentToBePopped,
59+
animated = state.changeRouteAnimated) {}
60+
this.routables.removeAt(routingAction.responsibleRoutableIndex+1)
61+
}
62+
63+
64+
}
65+
66+
is push -> {
67+
mainThreadHandler.post {
68+
val newRoutable = this.routables[routingAction.responsibleRoutableIndex].
69+
pushRouteSegment(routeElementIdentifier = routingAction.segmentToBePushed,
70+
animated = state.changeRouteAnimated){}
71+
this.routables.add(newRoutable)
72+
}
73+
74+
}
75+
76+
is change -> {
77+
mainThreadHandler.post {
78+
this.routables[routingAction.responsibleRoutableIndex + 1] =
79+
this.routables[routingAction.responsibleRoutableIndex].
80+
changeRouteSegment(from = routingAction.segmentToBeReplaced,
81+
to = routingAction.newSegment,
82+
animated = state.changeRouteAnimated){}
83+
}
84+
85+
}
86+
87+
}
88+
}
89+
90+
}
91+
92+
// Route Transformation Logic
93+
companion object {
94+
private fun largestCommonSubroute(oldRoute: Route, newRoute: Route): Int {
95+
var largestCommonSubroute = -1
96+
97+
while(largestCommonSubroute + 1 < newRoute.count() &&
98+
largestCommonSubroute + 1 < oldRoute.count() &&
99+
newRoute[largestCommonSubroute + 1] == oldRoute[largestCommonSubroute + 1]){
100+
largestCommonSubroute += 1
101+
}
102+
103+
return largestCommonSubroute
104+
}
105+
106+
107+
// Maps Route index to Routable index. Routable index is offset by 1 because the root Routable
108+
// is not represented in the route, e.g.
109+
// route = ["tabBar"]
110+
// routables = [RootRoutable, TabBarRoutable]
111+
112+
private fun routableIndexForRouteSegment(segment: Int): Int {
113+
return segment + 1
114+
}
115+
116+
fun routingActionsForTransitionFrom(oldRoute: Route, newRoute: Route) : ArrayList<RoutingAction> {
117+
118+
val routingActions = arrayListOf<RoutingAction>()
119+
120+
// Find the last common subroute between two routes
121+
val commonSubroute = largestCommonSubroute(oldRoute, newRoute)
122+
123+
if(commonSubroute == oldRoute.count() - 1 && commonSubroute == newRoute.count() - 1) {
124+
return arrayListOf()
125+
}
126+
// Keeps track which element of the routes we are working on
127+
// We start at the end of the old route
128+
var routeBuildingIndex = oldRoute.count() - 1
129+
130+
// Pop all route segments of the old route that are no longer in the new route
131+
// Stop one element ahead of the commonSubroute. When we are one element ahead of the
132+
// commmon subroute we have three options:
133+
//
134+
// 1. The old route had an element after the commonSubroute and the new route does not
135+
// we need to pop the route segment after the commonSubroute
136+
// 2. The old route had no element after the commonSubroute and the new route does, we
137+
// we need to push the route segment(s) after the commonSubroute
138+
// 3. The new route has a different element after the commonSubroute, we need to replace
139+
// the old route element with the new one
140+
while(routeBuildingIndex > commonSubroute + 1) {
141+
val routeSegmentToPop = oldRoute[routeBuildingIndex]
142+
143+
val popAction = pop(routableIndexForRouteSegment(routeBuildingIndex - 1),
144+
routeSegmentToPop)
145+
routingActions.add(popAction)
146+
routeBuildingIndex -= 1
147+
}
148+
149+
// This is the 1. case:
150+
// "The old route had an element after the commonSubroute and the new route does not
151+
// we need to pop the route segment after the commonSubroute"
152+
if(oldRoute.count() > newRoute.count()) {
153+
val popAction = pop(routableIndexForRouteSegment(routeBuildingIndex - 1),
154+
oldRoute[routeBuildingIndex])
155+
156+
//routingActions = routingActions.plus(popAction)
157+
routingActions.add(popAction)
158+
routeBuildingIndex -= 1
159+
}
160+
// This is the 3. case:
161+
// "The new route has a different element after the commonSubroute, we need to replace
162+
// the old route element with the new one"
163+
else if((oldRoute.count() > (commonSubroute + 1))
164+
&& (newRoute.count() > (commonSubroute + 1))) {
165+
val changeAction = change(routableIndexForRouteSegment(commonSubroute),
166+
oldRoute[commonSubroute + 1],
167+
newRoute[commonSubroute + 1])
168+
169+
routingActions.add(changeAction)
170+
}
171+
172+
// Push remainder of elements in new Route that weren't in old Route, this covers
173+
// the 2. case:
174+
// "The old route had no element after the commonSubroute and the new route does,
175+
// we need to push the route segment(s) after the commonSubroute"
176+
val newRouteIndex = newRoute.count() - 1
177+
178+
while(routeBuildingIndex < newRouteIndex) {
179+
val routeSegmentToPush = newRoute[routeBuildingIndex + 1]
180+
181+
val pushAction = push(routableIndexForRouteSegment(routeBuildingIndex),
182+
routeSegmentToPush)
183+
184+
routingActions.add(pushAction)
185+
routeBuildingIndex += 1
186+
}
187+
188+
return routingActions
189+
}
190+
191+
192+
}
193+
194+
}
195+
196+
197+
198+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.rekotlinrouter
2+
3+
import tw.geothings.rekotlin.*
4+
5+
/**
6+
This is ReKotlin's built in action type, it is the only built in type that conforms to the
7+
`Action` protocol. `StandardAction` can be serialized and can therefore be used with developer
8+
tools that restore state between app launches.
9+
10+
11+
It is recommended that you define your own types that conform to `Action` - if you want to be able
12+
to serialize your custom action types, you can implement `StandardActionConvertible` which will
13+
make it possible to generate a `StandardAction` from your typed action - the best of both worlds!
14+
*/
15+
class StandardAction(val type: String,
16+
val payload: Map<String,Any>? = null,
17+
val isTypedAction: Boolean = false ): Action
18+
19+
20+
/// Implement this protocol on your custom `Action` type if you want to make the action
21+
/// serializable.
22+
/// - Note: We are working on a tool to automatically generate the implementation of this protocol
23+
/// for your custom action types.
24+
interface StandardActionConvertible: Action {
25+
26+
/**
27+
Within this initializer you need to use the payload from the `StandardAction` to configure the
28+
state of your custom action type.
29+
30+
Example:
31+
32+
```
33+
init(_ standardAction: StandardAction) {
34+
this.twitterUser = standardAction.payload!["twitterUser"]!!
35+
}
36+
```
37+
38+
- Note: If you, as most developers, only use action serialization/deserialization during
39+
development, you can feel free to use the unsafe `!` operator.
40+
*/
41+
42+
43+
//fun init( standardAction: StandardAction)
44+
45+
/**
46+
Use the information from your custom action to generate a `StandardAction`. The `type` of the
47+
StandardAction should typically match the type name of your custom action type. You also need
48+
to set `isTypedAction` to `true`. Use the information from your action's properties to
49+
configure the payload of the `StandardAction`.
50+
51+
Example:
52+
53+
```
54+
fun toStandardAction() -> StandardAction {
55+
val payload = ["twitterUser": this.twitterUser]
56+
57+
return StandardAction(type = SearchTwitterScene.SetSelectedTwitterUser.type,
58+
payload = payload, isTypedAction = true)
59+
}
60+
```
61+
62+
*/
63+
fun toStandardAction(): StandardAction
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<resources>
2+
<string name="app_name">rekrouter</string>
3+
</resources>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Created by Mohanraj Karatadipalayam on 28/09/17.
3+
*/
4+
5+
package org.rekotlinrouter
6+
7+
import android.os.Handler
8+
import android.os.Looper
9+
import org.mockito.ArgumentMatchers
10+
import org.mockito.Mockito.*
11+
import org.mockito.stubbing.Answer
12+
import org.powermock.api.mockito.PowerMockito
13+
import java.util.concurrent.Executors
14+
import java.util.concurrent.TimeUnit
15+
16+
/**
17+
* Utility methods that unit tests can use to do common android library mocking that might be needed.
18+
*/
19+
object AndroidMockUtil {
20+
/**
21+
* Mocks main thread handler post() and postDelayed() for use in Android unit tests
22+
23+
* To use this:
24+
*
25+
* 1. Call this method in an @Before method of your test.
26+
* 2. Place Looper.class in the @PrepareForTest annotation before your test class.
27+
* 3. any class under test that needs to call `new Handler(Looper.getMainLooper())` should be placed
28+
* in the @PrepareForTest annotation as well.
29+
*
30+
31+
* @throws Exception
32+
*/
33+
@Throws(Exception::class)
34+
fun mockMainThreadHandler() {
35+
36+
// Mock the Looper
37+
PowerMockito.mockStatic(Looper::class.java)
38+
val mockMainThreadLooper = mock(Looper::class.java)
39+
`when`(Looper.getMainLooper()).thenReturn(mockMainThreadLooper)
40+
41+
// Mock the Handler
42+
val mockMainThreadHandler = mock(Handler::class.java)
43+
44+
val handlerPostAnswer = Answer { invocation ->
45+
val runnable = invocation.getArgument<Runnable>(0)
46+
var delay: Long? = 0L
47+
if (invocation.arguments.size > 1) {
48+
delay = invocation.getArgument<Long>(1)
49+
}
50+
if (runnable != null) {
51+
mainThread.schedule(runnable, delay!!, TimeUnit.MILLISECONDS)
52+
}
53+
true
54+
}
55+
56+
doAnswer(handlerPostAnswer).`when`(mockMainThreadHandler).post(ArgumentMatchers.any(Runnable::class.java))
57+
doAnswer(handlerPostAnswer).`when`(mockMainThreadHandler).postDelayed(ArgumentMatchers.any(Runnable::class.java), ArgumentMatchers.anyLong())
58+
PowerMockito.whenNew(Handler::class.java).withArguments(mockMainThreadLooper).thenReturn(mockMainThreadHandler)
59+
}
60+
61+
private val mainThread = Executors.newSingleThreadScheduledExecutor()
62+
63+
}
64+

‎rekrouter/src/test/java/org/rekotlinrouter/ReKotlinRouterIntegerationTests.kt

+403
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package org.rekotlinrouter
2+
3+
4+
import org.assertj.core.api.Assertions.assertThat
5+
import org.junit.Test
6+
//import org.junit.jupiter.api.DisplayName
7+
//import org.junit.jupiter.api.Test
8+
import tw.geothings.rekotlin.StateType
9+
10+
11+
/**
12+
* Created by mkaratadipalayam on 27/09/17.
13+
*/
14+
15+
internal class AppState: StateType
16+
17+
internal class ReKotlinRouterUnitTests {
18+
19+
val mainActivityIdentifier = "MainActivity"
20+
val counterActivityIdentifier = "CounterActivity"
21+
val statsActivityIdentifier = "StatsActivity"
22+
val infoActivityIdentifier = "InfoActivity"
23+
24+
25+
@Test
26+
//@DisplayName("calculates transitions from an empty route to a multi segment route")
27+
fun test_transition_from_empty_to_multi_segment_route(){
28+
29+
// Given
30+
val oldRoute: Route = arrayListOf()
31+
val newRoute = arrayListOf(mainActivityIdentifier, statsActivityIdentifier)
32+
33+
// When
34+
val routingActions = Router.routingActionsForTransitionFrom(oldRoute, newRoute)
35+
36+
// Then
37+
var action1Correct: Boolean = false
38+
var action2Correct: Boolean = false
39+
40+
routingActions.forEach { routingAction ->
41+
when(routingAction) {
42+
is push -> {
43+
if(routingAction.responsibleRoutableIndex==0 && routingAction.segmentToBePushed == mainActivityIdentifier ){
44+
action1Correct = true
45+
}
46+
if(routingAction.responsibleRoutableIndex==1 && routingAction.segmentToBePushed == statsActivityIdentifier ){
47+
action2Correct = true
48+
}
49+
}
50+
}
51+
}
52+
assertThat(action1Correct).isTrue()
53+
assertThat(action2Correct).isTrue()
54+
assertThat(routingActions.count()).isEqualTo(2)
55+
}
56+
57+
58+
@Test
59+
// @DisplayName("generates a Change action on the last common subroute")
60+
fun test_change_action_on_last_common_subroute(){
61+
62+
// Given
63+
val oldRoute = arrayListOf(mainActivityIdentifier,counterActivityIdentifier)
64+
val newRoute = arrayListOf(mainActivityIdentifier,statsActivityIdentifier)
65+
66+
// When
67+
val routingActions = Router.routingActionsForTransitionFrom(oldRoute, newRoute)
68+
69+
// Then
70+
var controllerIndex: Int = -1
71+
var toBeReplaced: RouteElementIdentifier=""
72+
var new: RouteElementIdentifier=""
73+
routingActions.forEach { routingAction ->
74+
75+
when (routingAction) {
76+
is change -> {
77+
controllerIndex = routingAction.responsibleRoutableIndex
78+
toBeReplaced = routingAction.segmentToBeReplaced
79+
new = routingAction.newSegment
80+
}
81+
}
82+
}
83+
assertThat(controllerIndex).isEqualTo(1)
84+
assertThat(toBeReplaced).isEqualTo(counterActivityIdentifier)
85+
assertThat(new).isEqualTo(statsActivityIdentifier)
86+
}
87+
88+
@Test
89+
// @DisplayName("generates a Change action on the last common subroute, also for routes of different length")
90+
fun test_change_action_on_last_common_subroute_plus_routes_of_different_length(){
91+
92+
// Given
93+
val oldRoute = arrayListOf(mainActivityIdentifier,counterActivityIdentifier)
94+
val newRoute = arrayListOf(mainActivityIdentifier,statsActivityIdentifier,infoActivityIdentifier)
95+
96+
// When
97+
val routingActions = Router.routingActionsForTransitionFrom(oldRoute, newRoute)
98+
99+
// Then
100+
var action1Correct: Boolean = false
101+
var action2Correct: Boolean = false
102+
103+
routingActions.forEach { routingAction ->
104+
when(routingAction) {
105+
is change -> {
106+
if(routingAction.responsibleRoutableIndex==1
107+
&& routingAction.segmentToBeReplaced == counterActivityIdentifier
108+
&& routingAction.newSegment == statsActivityIdentifier){
109+
action1Correct = true
110+
}
111+
}
112+
is push -> {
113+
if(routingAction.responsibleRoutableIndex==2 && routingAction.segmentToBePushed == infoActivityIdentifier ){
114+
action2Correct = true
115+
}
116+
117+
}
118+
119+
}
120+
}
121+
122+
assertThat(action1Correct).isTrue()
123+
assertThat(action2Correct).isTrue()
124+
assertThat(routingActions.count()).isEqualTo(2)
125+
}
126+
127+
@Test
128+
// @DisplayName("generates a Change action on root when root element changes")
129+
fun test_change_action_on_root_when_root_element_changes(){
130+
131+
// Given
132+
val oldRoute = arrayListOf(mainActivityIdentifier)
133+
val newRoute = arrayListOf(statsActivityIdentifier)
134+
135+
// When
136+
val routingActions = Router.routingActionsForTransitionFrom(oldRoute, newRoute)
137+
138+
// Then
139+
var controllerIndex: Int = -1
140+
var toBeReplaced: RouteElementIdentifier=""
141+
var new: RouteElementIdentifier=""
142+
routingActions.forEach { routingAction ->
143+
144+
when (routingAction) {
145+
is change -> {
146+
controllerIndex = routingAction.responsibleRoutableIndex
147+
toBeReplaced = routingAction.segmentToBeReplaced
148+
new = routingAction.newSegment
149+
}
150+
}
151+
}
152+
assertThat(controllerIndex).isEqualTo(0)
153+
assertThat(routingActions.count()).isEqualTo(1)
154+
assertThat(toBeReplaced).isEqualTo(mainActivityIdentifier)
155+
assertThat(new).isEqualTo(statsActivityIdentifier)
156+
}
157+
158+
@Test
159+
// @DisplayName("calculates no actions for transition from empty route to empty route")
160+
fun test_no_action_when_transistion_from_empty_to_empty_route(){
161+
162+
// Given
163+
// val oldRoute: Route = emptyArray()
164+
// val newRoute: Route = emptyArray()
165+
166+
val oldRoute: Route = arrayListOf()
167+
val newRoute: Route = arrayListOf()
168+
// When
169+
val routingActions = Router.routingActionsForTransitionFrom(oldRoute, newRoute)
170+
171+
// Then
172+
assertThat(routingActions.count()).isEqualTo(0)
173+
}
174+
175+
@Test
176+
// @DisplayName("calculates no actions for transitions between identical, non-empty routes")
177+
fun test_no_action_when_transistion_from_identical_non_empty_routes(){
178+
179+
// Given
180+
val oldRoute = arrayListOf(mainActivityIdentifier,counterActivityIdentifier)
181+
val newRoute = arrayListOf(mainActivityIdentifier,counterActivityIdentifier)
182+
183+
// When
184+
val routingActions = Router.routingActionsForTransitionFrom(oldRoute, newRoute)
185+
186+
// Then
187+
assertThat(routingActions.count()).isEqualTo(0)
188+
}
189+
190+
@Test
191+
//@DisplayName("calculates transitions with multiple pops")
192+
fun test_transistion_with_multiple_pops(){
193+
194+
// Given
195+
val oldRoute = arrayListOf(mainActivityIdentifier,statsActivityIdentifier,counterActivityIdentifier)
196+
val newRoute = arrayListOf(mainActivityIdentifier)
197+
198+
// When
199+
val routingActions = Router.routingActionsForTransitionFrom(oldRoute, newRoute)
200+
201+
// Then
202+
var action1Correct: Boolean = false
203+
var action2Correct: Boolean = false
204+
routingActions.forEach { routingAction ->
205+
when(routingAction) {
206+
is pop -> {
207+
if(routingAction.responsibleRoutableIndex==2 && routingAction.segmentToBePopped == counterActivityIdentifier ){
208+
action1Correct = true
209+
}
210+
if(routingAction.responsibleRoutableIndex==1 && routingAction.segmentToBePopped == statsActivityIdentifier ){
211+
action2Correct = true
212+
}
213+
}
214+
}
215+
}
216+
assertThat(action1Correct).isTrue()
217+
assertThat(action2Correct).isTrue()
218+
assertThat(routingActions.count()).isEqualTo(2)
219+
}
220+
221+
@Test
222+
// @DisplayName("calculates transitions with multiple pushes")
223+
fun test_transistions_with_multiple_pushes(){
224+
225+
// Given
226+
val oldRoute = arrayListOf(mainActivityIdentifier)
227+
val newRoute = arrayListOf(mainActivityIdentifier,statsActivityIdentifier,counterActivityIdentifier)
228+
229+
// When
230+
val routingActions = Router.routingActionsForTransitionFrom(oldRoute, newRoute)
231+
232+
// Then
233+
var action1Correct: Boolean = false
234+
var action2Correct: Boolean = false
235+
routingActions.forEach { routingAction ->
236+
when(routingAction) {
237+
is push -> {
238+
if(routingAction.responsibleRoutableIndex==1 && routingAction.segmentToBePushed == statsActivityIdentifier ){
239+
action1Correct = true
240+
}
241+
if(routingAction.responsibleRoutableIndex==2 && routingAction.segmentToBePushed == counterActivityIdentifier ){
242+
action2Correct = true
243+
}
244+
245+
}
246+
}
247+
}
248+
assertThat(action1Correct).isTrue()
249+
assertThat(action2Correct).isTrue()
250+
assertThat(routingActions.count()).isEqualTo(2)
251+
}
252+
}

‎settings.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
//include ':app', ':rekrouter'
2+
include ':rekrouter'

0 commit comments

Comments
 (0)
Please sign in to comment.