├── .github ├── dependabot.yml └── workflows │ └── android.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── android-test ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── nolambda │ └── linkrouter │ └── android │ └── test │ ├── DeeplinkTestHelper.kt │ ├── RouterTestHelper.kt │ ├── TestRoute.kt │ └── TestRouteWithParam.kt ├── android ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── nolambda │ │ │ └── linkrouter │ │ │ └── android │ │ │ ├── AbstractAppRouter.kt │ │ │ ├── RouteResult.kt │ │ │ ├── RouterExt.kt │ │ │ ├── RouterInitializer.kt │ │ │ ├── RouterInterface.kt │ │ │ ├── RouterPlugin.kt │ │ │ ├── Routes.kt │ │ │ ├── UriResult.kt │ │ │ ├── UriRouterFactory.kt │ │ │ ├── autoregister │ │ │ ├── AutoRegister.kt │ │ │ ├── RouteAutoRegisterMiddleware.kt │ │ │ └── RouteInit.kt │ │ │ ├── measure │ │ │ ├── DefaultMeasureConfig.kt │ │ │ ├── MeasureConfig.kt │ │ │ ├── MeasureMiddleWare.kt │ │ │ └── MeasuredAbstractAppRouter.kt │ │ │ ├── middlewares │ │ │ ├── MiddleWareResult.kt │ │ │ └── Middleware.kt │ │ │ └── registerstrategy │ │ │ ├── EagerRegisterStrategy.kt │ │ │ ├── LazyRegisterStrategy.kt │ │ │ └── RegisterStrategy.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── nolambda │ └── linkrouter │ └── android │ ├── AndroidPerformanceTest.kt │ ├── DeepLinkSpec.kt │ ├── MiddleWareSpec.kt │ ├── RouterAutoRegisterMiddlewareSpec.kt │ └── RouterSpec.kt ├── annotations ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── nolambda │ └── linkrouter │ └── annotations │ └── Navigate.kt ├── art └── diagram.png ├── build.gradle ├── buildSystem ├── base.gradle ├── dependencies.gradle └── stdlib.gradle ├── core ├── .gitignore ├── .kotlintest │ └── spec_failures ├── build.gradle └── src │ ├── main │ └── java │ │ └── nolambda │ │ └── linkrouter │ │ ├── DeepLinkEntry.kt │ │ ├── DeepLinkUri.kt │ │ ├── Hostname.kt │ │ ├── KeyUriRouter.kt │ │ ├── MapOpt.kt │ │ ├── RoutePath.kt │ │ ├── SimpleRouter.kt │ │ ├── SimpleUriRouter.kt │ │ ├── UriRouter.kt │ │ ├── Util.kt │ │ ├── error │ │ └── RouteNotFoundException.kt │ │ └── matcher │ │ ├── DeepLinkEntryMatcher.kt │ │ └── UriMatcher.kt │ └── test │ └── java │ └── nolambda │ └── linkrouter │ ├── DeepLinkUriSpec.kt │ ├── PerformanceTest.kt │ ├── RoutePathSpec.kt │ ├── SampleDeeplinkTest.kt │ └── UriRouterSpec.kt ├── extras ├── android-activityresult │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── nolambda │ │ └── linkrouter │ │ └── android │ │ └── extra │ │ └── caller │ │ ├── AcitivtyResultRouterExt.kt │ │ ├── ActivityResultLauncherMiddleWare.kt │ │ ├── ActivityResultLauncherProcessor.kt │ │ ├── Helper.kt │ │ └── scenario │ │ ├── Scenario.kt │ │ ├── ScenarioExt.kt │ │ ├── ScenarioHost.kt │ │ ├── launcher │ │ ├── ParameterizedScenarioLauncher.kt │ │ └── ScenarioLauncher.kt │ │ └── processor │ │ ├── ComposedResultProcessor.kt │ │ ├── OnResult.kt │ │ ├── ResultProcessorIndexTracker.kt │ │ ├── RetainedComposedResultProcessor.kt │ │ ├── RetainedScenarioResultProcessor.kt │ │ └── ScenarioResultProcessor.kt └── android-caller │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── nolambda │ │ └── linkrouter │ │ └── android │ │ └── extra │ │ └── caller │ │ ├── CallerAppRouter.kt │ │ ├── CallerProcessor.kt │ │ └── CallerProviderMiddleware.kt │ └── test │ └── java │ └── nolambda │ └── linkrouter │ └── android │ └── extra │ └── caller │ └── CallerAppRouterSpec.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── processor ├── .gitignore ├── .kotlintest │ └── spec_failures ├── build.gradle └── src │ ├── main │ └── java │ │ └── nolambda │ │ └── linkrouter │ │ └── processor │ │ ├── ProcessorExt.kt │ │ ├── RouteInitGenerator.kt │ │ ├── RouteInitNode.kt │ │ └── RouteInitProcessor.kt │ └── test │ ├── java │ └── nolambda │ │ └── linkrouter │ │ └── processor │ │ └── RouteInitGeneratorSpec.kt │ └── resources │ └── manifest.xml ├── samples ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ ├── src │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── nolambda │ │ │ │ └── linkrouter │ │ │ │ └── examples │ │ │ │ ├── PerformanceTest.kt │ │ │ │ └── PerformanceTestScreen.kt │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── nolambda │ │ │ │ └── linkrouter │ │ │ │ └── examples │ │ │ │ ├── FragmentRouter.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── PerformanceTestActivity.kt │ │ │ │ ├── SampleApp.kt │ │ │ │ ├── SampleFragment.kt │ │ │ │ ├── notsimple │ │ │ │ ├── HomeScreen.kt │ │ │ │ └── NotSimpleActivity.kt │ │ │ │ ├── picker │ │ │ │ ├── PickerExampleFragmentScreen.kt │ │ │ │ ├── PickerExampleScreen.kt │ │ │ │ ├── ResultPickerRoute.kt │ │ │ │ └── ResultPickerScreen.kt │ │ │ │ └── utils │ │ │ │ └── Utils.kt │ │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ ├── activity_main_simple.xml │ │ │ ├── activity_performance_test.xml │ │ │ ├── activity_picker_example.xml │ │ │ ├── activity_result_picker.xml │ │ │ ├── fragment_sample.xml │ │ │ └── fragment_sample_home.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── 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 │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ └── universalrouter.jks ├── approuter │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── nolambda │ │ │ └── linkrouter │ │ │ └── approuter │ │ │ ├── AppRoutes.kt │ │ │ └── PathCreator.kt │ │ └── res │ │ └── values │ │ └── strings.xml ├── build.gradle ├── cart │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── nolambda │ │ │ └── linkrouter │ │ │ └── cart │ │ │ ├── CartRouterInitializer.kt │ │ │ └── CartScreen.kt │ │ └── res │ │ ├── layout │ │ └── fragment_cart.xml │ │ └── values │ │ └── strings.xml ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── product │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── nolambda │ │ │ └── linkrouter │ │ │ └── product │ │ │ ├── ProductRouterInitializer.kt │ │ │ └── ProductScreen.kt │ │ └── res │ │ ├── layout │ │ └── fragment_product.xml │ │ └── values │ │ └── strings.xml └── settings.gradle ├── settings.gradle └── setup.sh /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Updates for Github Actions used in the repo 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | reviewers: 12 | - "esafirm" 13 | # Updates for Gradle dependencies used in the app 14 | - package-ecosystem: gradle 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | open-pull-requests-limit: 10 19 | reviewers: 20 | - "esafirm" -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: androidci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2.4.0 15 | - name: set up JDK 11 16 | uses: actions/setup-java@v2.4.0 17 | with: 18 | distribution: adopt-openj9 19 | java-version: '11' 20 | - name: Build with Gradle 21 | run: ./gradlew build 22 | - name: Test 23 | run: | 24 | ./gradlew :core:test 25 | ./gradlew :processor:test 26 | ./gradlew :android:testDebugUnitTest 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Esa Firman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: refresh 2 | refresh: 3 | ./gradlew install 4 | cd samples 5 | ./gradlew tasks --refresh-dependencies 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal Router 2 | 3 | ![](https://github.com/esafirm/universal-router/workflows/androidci/badge.svg) 4 | 5 | > Router for every ocassion ~ 6 | 7 | Universal router comes with two flavor, the core module which basically a link router that can convert your URI to whatever you need. And the Android module which more opinionated to how you can use it to help you solve your navigation problem 8 | 9 | ## Download 10 | 11 | Add this to your project `build.gradle` 12 | 13 | ```groovy 14 | allprojects { 15 | repositories { 16 | maven { url "https://jitpack.io" } 17 | } 18 | } 19 | ``` 20 | 21 | And add this to your module `build.gradle` 22 | 23 | ```groovy 24 | dependencies { 25 | implementation "com.github.esafirm.universal-router:core:$routerVersion" 26 | implementation "com.github.esafirm.universal-router:android:$routerVersion" 27 | } 28 | ``` 29 | 30 | ## Core 31 | 32 | It basically consist of two router 33 | 34 | 1. `SimpleRouter` which route `Any` type of object to anything you need 35 | 2. `UrlRouter` which takes URI instead of object 36 | 37 | ### Some Examples 38 | 39 | ```kotlin 40 | // Define router 41 | class StringRouter : UrlRouter() { 42 | 43 | init { 44 | addEntry("nolambda://test/{a}/{b}", "https://test/{a}/{b}") { _, param -> 45 | val first = param["a"] 46 | val second = param["b"] 47 | "$second came to the wrong neighborhood $first" 48 | } 49 | } 50 | } 51 | 52 | // Call router 53 | // This will return string "you can to the wrong neighborhood yo" 54 | StringRouter().resolve("nolambda://test/yo/you") 55 | ``` 56 | 57 | > For more sample, plese look at the `samples` directory. 58 | 59 | ## Android 60 | 61 | 62 | 63 | Basically with just the `core` module you already can have a navigation system in your modular structured application (think dynamic module use case). The easiest way would be creating a `Singleton` router in your "core" module and then add entries in every other module, but this can get quite messy sometimes, this is when the android router module comes in. 64 | 65 | First let's define our project structure: 66 | 67 | ```kotlin 68 | project 69 | │ 70 | ├── app // Android app module, depends to all modules 71 | │ 72 | ├── cart // Feature cart, only depends to router 73 | │ 74 | ├── product // Feature product, only depends to router 75 | │ 76 | └── approuter // Router libs that every module depends 77 | ``` 78 | 79 | > In dynamic module use case the `cart` and `product` module would be depends the `app` module 80 | 81 | Next what you want to create is the list of the routes in the "router" module, in this case `approuter` 82 | 83 | ```kotlin 84 | object AppRouter { 85 | // Simplest form of Route 86 | object Home : Route() 87 | // Route support deeplink navigation 88 | object Cart : Route("https://bukatoko.com/cart") 89 | // Route also support navigation with parameter 90 | object Product : RouteWithParam( 91 | paths = arrayOf("https://bukatoko.com/{product_id}", "app://product/{id}"), 92 | ) { 93 | data class ProductParam( 94 | val productId: String 95 | ) 96 | } 97 | } 98 | ``` 99 | 100 | After that, you have to register your navigation logic to the `Route` 101 | 102 | ```kotlin 103 | AppRouter.Cart.Register { 104 | context.startActivity(Intent(context, CartScreen::class.java)) 105 | } 106 | ``` 107 | 108 | If you want to initiate this in startup and your module doesn't have the access to `Application` class you can use the initializer 109 | 110 | ```kotlin 111 | class CartRouterInitializer : RouterInitializer { 112 | override fun onInit(appContext: Context) { 113 | ... // do as above 114 | } 115 | } 116 | ``` 117 | 118 | Don't forget to register this on manifest 119 | 120 | ```xml 121 | 125 | ``` 126 | 127 | This is actually it if your navigation logic nature is "fire and forget", but in case you have to get something back (like `Fragment`) and use it in other place you can use the `RouteProcessor` 128 | 129 | ```kotlin 130 | // Processor only process return that has the same type as passed class 131 | // In this case it will only process router that return Fragment 132 | Router.addProcessor { 133 | supportManager.beginTransaction() 134 | .replace(R.id.container, it) 135 | .commit() 136 | } 137 | ``` 138 | 139 | After that you can use the `Router` to navigate your app 140 | 141 | ```kotlin 142 | // This will trigger Cart register lambda 143 | Router.push(AppRouter.Cart) 144 | 145 | // You can use the registered deeplink too 146 | Router.goTo("https://bukatoko.com/cart") 147 | ``` 148 | 149 | And that's it you got yourself a navigation system. 150 | 151 | > I can't stress enough that you should check `samples` for better understanding of the library 152 | 153 | ## What's Next 154 | 155 | - Navigation type (push, replace, pop) 156 | - Annotation auto register (It's partially working now) 157 |  158 | 159 | ## License 160 | 161 | MIT @ Esa Firman 162 | -------------------------------------------------------------------------------- /android-test/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /android-test/build.gradle: -------------------------------------------------------------------------------- 1 | apply from: "${buildSystemDir}/base.gradle" 2 | apply plugin: 'maven-publish' 3 | 4 | dependencies { 5 | implementation project(':android') 6 | } 7 | 8 | afterEvaluate { 9 | publishing { 10 | publications { 11 | release(MavenPublication) { 12 | from components.release 13 | 14 | groupId = appGroupId 15 | artifactId = project.name 16 | version = appVersion 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /android-test/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esafirm/universal-router/9fa8b8a1e0f7d728e4b229105b1a7ceeb0d700b0/android-test/consumer-rules.pro -------------------------------------------------------------------------------- /android-test/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /android-test/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /android-test/src/main/java/nolambda/linkrouter/android/test/DeeplinkTestHelper.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.test 2 | 3 | import nolambda.linkrouter.android.AppRouter 4 | import nolambda.linkrouter.android.BaseRoute 5 | import nolambda.linkrouter.android.RouteParam 6 | 7 | data class DeepLinkAssertionData

( 8 | val routeParam: RouteParam?, 9 | val route: BaseRoute

, 10 | val currentPath: String, 11 | val error: Throwable? = null 12 | ) 13 | 14 | typealias DeepLinkAssertion = (DeepLinkAssertionData) -> Unit 15 | typealias DeepLinkTestPair = Pair> 16 | typealias DeepLinkRoute = Pair, List>> 17 | 18 | fun

AppRouter.testDeepLink( 19 | deepLinkData: List>, 20 | clean: Boolean = true 21 | ) { 22 | val router = this 23 | if (clean) { 24 | router.cleanRouter() 25 | } 26 | 27 | deepLinkData.forEach { (route, testData) -> 28 | var lastRouteParam: RouteParam? 29 | var error: Throwable? = null 30 | router.register(route) { 31 | lastRouteParam = it 32 | } 33 | testData.forEach { (path, assertion) -> 34 | lastRouteParam = null 35 | try { 36 | router.goTo(path) 37 | } catch (e: Throwable) { 38 | error = e 39 | } 40 | assertion(DeepLinkAssertionData( 41 | routeParam = lastRouteParam, 42 | route = route, 43 | currentPath = path, 44 | error = error 45 | )) 46 | } 47 | } 48 | } 49 | 50 | object DeepLinkAssertions { 51 | fun

shouldValid(isValid: Boolean = true): DeepLinkAssertion = { p -> 52 | check(p.routeParam != null == isValid) { "DeepLink is not handled: ${p.currentPath}" } 53 | } 54 | 55 | /** 56 | * This assertion only works if you're not setting a custom error handler to Router plugin 57 | * 58 | */ 59 | inline fun shouldThrow(): DeepLinkAssertion<*, *> = { p -> 60 | check(p.error is T) 61 | } 62 | } -------------------------------------------------------------------------------- /android-test/src/main/java/nolambda/linkrouter/android/test/RouterTestHelper.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.test 2 | 3 | import nolambda.linkrouter.android.AbstractAppRouter 4 | import nolambda.linkrouter.android.Route 5 | import nolambda.linkrouter.android.RouteParam 6 | import nolambda.linkrouter.android.RouteWithParam 7 | 8 | typealias RouterMatcher = (RouteParam) -> Boolean 9 | 10 | @Suppress("UNCHECKED_CAST") 11 | fun AbstractAppRouter.testHit( 12 | route: Route, 13 | matcher: RouterMatcher = RouterMatchers.hitMatcher(), 14 | ): Boolean { 15 | var testResult = false 16 | register(route) { 17 | testResult = matcher.invoke(it) 18 | } 19 | push(route) 20 | return testResult 21 | } 22 | 23 | @Suppress("UNCHECKED_CAST") 24 | fun

AbstractAppRouter.testHit( 25 | route: RouteWithParam

, 26 | param: P, 27 | matcher: RouterMatcher = RouterMatchers.hitMatcher(), 28 | ): Boolean { 29 | var testResult = false 30 | register(route) { 31 | testResult = matcher.invoke(it) 32 | } 33 | push(route, param) 34 | return testResult 35 | } 36 | 37 | object RouterMatchers { 38 | fun hitMatcher(): RouterMatcher = { true } 39 | } -------------------------------------------------------------------------------- /android-test/src/main/java/nolambda/linkrouter/android/test/TestRoute.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.test 2 | 3 | import nolambda.linkrouter.android.Route 4 | 5 | open class TestRoute( 6 | vararg paths: String = emptyArray() 7 | ) : Route(*paths) -------------------------------------------------------------------------------- /android-test/src/main/java/nolambda/linkrouter/android/test/TestRouteWithParam.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.test 2 | 3 | import nolambda.linkrouter.android.RouteWithParam 4 | 5 | open class TestRouteWithParam( 6 | vararg paths: String = emptyArray() 7 | ) : RouteWithParam(*paths) -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .kotlintest 3 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply from: "${buildSystemDir}/base.gradle" 2 | apply plugin: 'maven-publish' 3 | 4 | dependencies { 5 | implementation project(':core') 6 | implementation deps.appCompat 7 | 8 | api deps.coroutine 9 | 10 | testImplementation testDeps 11 | testImplementation project(':android-test') 12 | } 13 | 14 | afterEvaluate { 15 | publishing { 16 | publications { 17 | release(MavenPublication) { 18 | from components.release 19 | 20 | groupId = appGroupId 21 | artifactId = project.name 22 | version = appVersion 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /android/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esafirm/universal-router/9fa8b8a1e0f7d728e4b229105b1a7ceeb0d700b0/android/consumer-rules.pro -------------------------------------------------------------------------------- /android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/AbstractAppRouter.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | 3 | package nolambda.linkrouter.android 4 | 5 | import nolambda.linkrouter.SimpleRouter 6 | import nolambda.linkrouter.android.middlewares.MiddleWareResult 7 | import nolambda.linkrouter.android.middlewares.Middleware 8 | import nolambda.linkrouter.android.registerstrategy.EagerRegisterStrategy 9 | import nolambda.linkrouter.android.registerstrategy.RegisterStrategy 10 | 11 | abstract class AbstractAppRouter( 12 | vararg middleWares: Middleware = emptyArray(), 13 | private val registerStrategy: RegisterStrategy = EagerRegisterStrategy(), 14 | private val uriRouterFactory: UriRouterFactory = SimpleUriRouterFactory() 15 | ) : AppRouter { 16 | 17 | private class AndroidSimpleRouter : SimpleRouter>() 18 | 19 | private val simpleRouter by lazy { AndroidSimpleRouter() } 20 | private val uriRouter by lazy { uriRouterFactory.create() } 21 | 22 | private val middlewares by lazy { createMiddleWares() } 23 | private val processors = linkedSetOf, RouteProcessor>>() 24 | 25 | private val _middleWares = middleWares 26 | 27 | protected open fun createMiddleWares(): Set> { 28 | val set = linkedSetOf>() 29 | set.addAll(_middleWares) 30 | return set 31 | } 32 | 33 | /* --------------------------------------------------- */ 34 | /* > Component setup */ 35 | /* --------------------------------------------------- */ 36 | 37 | override fun cleanRouter() { 38 | simpleRouter.clear() 39 | uriRouter.clear() 40 | processors.clear() 41 | } 42 | 43 | override fun removeProcessor(processor: RouteProcessor<*>) { 44 | processors.removeAll { it.second == processor } 45 | } 46 | 47 | override fun addProcessor(clazz: Class, processor: RouteProcessor) { 48 | processors.add(clazz to processor as RouteProcessor) 49 | } 50 | 51 | inline fun addProcessor(noinline processor: RouteProcessor) { 52 | addProcessor(T::class.java, processor) 53 | } 54 | 55 | /* --------------------------------------------------- */ 56 | /* > Processing */ 57 | /* --------------------------------------------------- */ 58 | 59 | override fun

register(route: BaseRoute

, handler: RouteHandler) { 60 | registerStrategy.register(simpleRouter, uriRouter, route, handler) 61 | } 62 | 63 | override fun resolveUri(uri: String): UriResult? { 64 | registerStrategy.await() 65 | return uriRouter.resolve(uri) 66 | } 67 | 68 | override fun canHandle(uri: String): Boolean { 69 | return resolveUri(uri) != null 70 | } 71 | 72 | override fun goTo(uri: String): RouteResult { 73 | return try { 74 | val result = resolveUri(uri) ?: return RouteResult(false) 75 | val (deepLinkUri, route, param) = result 76 | val routeParam = if (route is RouteWithParam<*>) { 77 | route.mapUri(deepLinkUri, param) 78 | } else null 79 | val routeResult = processRoute(route as BaseRoute, routeParam, createInfo(uri)) 80 | RouteResult(true, routeResult) 81 | } catch (e: Exception) { 82 | e.handleError() 83 | RouteResult(false) 84 | } 85 | } 86 | 87 | override fun

push(route: RouteWithParam

, param: P): RouteResult { 88 | return try { 89 | val result = processRoute(route, param, createInfo()) 90 | RouteResult(true, result) 91 | } catch (e: Exception) { 92 | e.handleError() 93 | RouteResult(false) 94 | } 95 | } 96 | 97 | override fun push(route: Route): RouteResult { 98 | return try { 99 | val result = processRoute(route, null, createInfo()) 100 | RouteResult(true, result) 101 | } catch (e: Exception) { 102 | e.handleError() 103 | RouteResult(false) 104 | } 105 | } 106 | 107 | private fun Throwable.handleError() = RouterPlugin.errorHandler(this) 108 | 109 | private fun

processRoute( 110 | route: BaseRoute

, 111 | param: P?, 112 | actionInfo: ActionInfo 113 | ): Any? { 114 | val routeParam = RouteParam( 115 | info = actionInfo, 116 | param = param 117 | ) 118 | val (finalRoute, finalParam) = applyMiddleware(route, routeParam) 119 | val result = simpleResolve(finalRoute, finalParam) 120 | invokeProcessor(result, actionInfo) 121 | return result 122 | } 123 | 124 | protected open fun

applyMiddleware( 125 | route: BaseRoute<*>, 126 | routeParam: RouteParam 127 | ): MiddleWareResult { 128 | val initial = MiddleWareResult(route, routeParam) 129 | return middlewares.fold(initial) { acc, middleware -> 130 | middleware.onRouting(acc.route, acc.routeParam) 131 | } 132 | } 133 | 134 | private fun simpleResolve( 135 | route: BaseRoute<*>, 136 | param: RouteParam<*, *> 137 | ) = simpleRouter.resolve(route).invoke(param) 138 | 139 | protected open fun invokeProcessor(result: Any?, info: ActionInfo) { 140 | if (result == null) return 141 | if (processors.isEmpty()) return 142 | 143 | val clazz = result.javaClass 144 | processors.forEach { (c, processor) -> 145 | if (c.isAssignableFrom(clazz)) { 146 | processor.invoke(result, info) 147 | } 148 | } 149 | } 150 | 151 | private fun createInfo(url: String? = null): ActionInfo = ActionInfo(this, url) 152 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/RouteResult.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | class RouteResult( 4 | val isHandled: Boolean, 5 | private val result: Any? = null 6 | ) { 7 | @Suppress("UNCHECKED_CAST") 8 | fun getResultOrError(): R = result as R 9 | 10 | fun getResultOrNull(): R? { 11 | return try { 12 | @Suppress("UNCHECKED_CAST") 13 | result as? R? 14 | } catch (e: Exception) { 15 | null 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/RouterExt.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | import androidx.lifecycle.Lifecycle 4 | import androidx.lifecycle.LifecycleObserver 5 | import androidx.lifecycle.LifecycleOwner 6 | import androidx.lifecycle.OnLifecycleEvent 7 | 8 | inline fun LifecycleOwner.addRouterProcessor( 9 | router: RouterComponents, 10 | noinline processor: RouteProcessor 11 | ) { 12 | lifecycle.addObserver(object : LifecycleObserver { 13 | @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) 14 | fun onCreate() { 15 | router.addProcessor(T::class.java, processor) 16 | } 17 | 18 | @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) 19 | fun onDestroy() { 20 | router.removeProcessor(processor) 21 | } 22 | }) 23 | 24 | if (lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { 25 | router.addProcessor(T::class.java, processor) 26 | } 27 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/RouterInitializer.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | import android.content.ContentProvider 4 | import android.content.ContentValues 5 | import android.content.Context 6 | import android.content.pm.ProviderInfo 7 | import android.database.Cursor 8 | import android.net.Uri 9 | 10 | abstract class RouterInitializer : ContentProvider() { 11 | 12 | override fun attachInfo(context: Context?, info: ProviderInfo?) { 13 | super.attachInfo(context, info) 14 | onInit(context!!) 15 | } 16 | 17 | abstract fun onInit(appContext: Context) 18 | 19 | override fun insert(uri: Uri, values: ContentValues?): Uri? = null 20 | 21 | override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? = null 22 | 23 | override fun onCreate() = false 24 | 25 | override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0 26 | 27 | override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 28 | 29 | override fun getType(uri: Uri): String? = null 30 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/RouterInterface.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | typealias RouteHandler = (RouteParam) -> R 4 | typealias RouteProcessor = (T, ActionInfo) -> Unit 5 | 6 | class RouteParam( 7 | val param: Param? = null, 8 | val info: ActionInfo, 9 | var extra: Extra? = null 10 | ) { 11 | fun

copyWithParam( 12 | passedParam: P?, 13 | passedInfo: ActionInfo = info, 14 | passedExtra: Extra? = extra 15 | ) = RouteParam(passedParam, passedInfo, passedExtra) 16 | } 17 | 18 | /** 19 | * Contain info regarding the action that trigger the routing 20 | */ 21 | data class ActionInfo( 22 | val currentRouter: AbstractAppRouter<*>, 23 | val uri: String? = null 24 | ) { 25 | val isTriggeredByUri = uri.isNullOrEmpty().not() 26 | } 27 | 28 | interface RouterComponents { 29 | fun cleanRouter() 30 | fun removeProcessor(processor: RouteProcessor<*>) 31 | fun addProcessor(clazz: Class, processor: RouteProcessor) 32 | } 33 | 34 | interface RouterProcessor { 35 | fun

register(route: BaseRoute

, handler: RouteHandler) 36 | 37 | /** 38 | * @param uri - URI to be resolved 39 | * @return null if there's no matching uri register in the router 40 | * otherwise return [UriResult] 41 | */ 42 | fun resolveUri(uri: String): UriResult? 43 | 44 | /** 45 | * @param uri - URI to be resolved 46 | * @return false if there's no matching uri 47 | */ 48 | fun canHandle(uri: String): Boolean 49 | 50 | 51 | fun goTo(uri: String): RouteResult 52 | fun push(route: Route): RouteResult 53 | fun

push(route: RouteWithParam

, param: P): RouteResult 54 | } 55 | 56 | interface AppRouter : RouterProcessor, RouterComponents -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/RouterPlugin.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | import android.content.Context 4 | 5 | object RouterPlugin { 6 | lateinit var appContext: Context 7 | var isUseAnnotationProcessor = false 8 | var logger: ((String) -> Unit)? = null 9 | var errorHandler: (Throwable) -> Unit = { throw it } 10 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/Routes.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | import nolambda.linkrouter.DeepLinkUri 4 | import nolambda.linkrouter.matcher.DeepLinkEntryMatcher 5 | import nolambda.linkrouter.matcher.UriMatcher 6 | 7 | abstract class BaseRoute

( 8 | vararg val routePaths: String 9 | ) { 10 | open fun pathMatcher(): UriMatcher = DeepLinkEntryMatcher 11 | } 12 | 13 | abstract class Route( 14 | vararg paths: String = emptyArray() 15 | ) : BaseRoute(*paths) 16 | 17 | abstract class RouteWithParam

( 18 | vararg paths: String = emptyArray() 19 | ) : BaseRoute

(*paths) { 20 | 21 | open fun mapUri(uri: DeepLinkUri, raw: Map): P { 22 | throw IllegalStateException("mapUri should be override if you have paths") 23 | } 24 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/UriResult.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | import nolambda.linkrouter.DeepLinkUri 4 | 5 | /** 6 | * URI Router resolved data 7 | */ 8 | data class UriResult( 9 | val uri: DeepLinkUri, 10 | val route: BaseRoute<*>, 11 | val param: Map 12 | ) 13 | -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/UriRouterFactory.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri 4 | import nolambda.linkrouter.KeyUriRouter 5 | import nolambda.linkrouter.SimpleUriRouter 6 | import nolambda.linkrouter.UriRouter 7 | import nolambda.linkrouter.UriRouterLogger 8 | import java.util.concurrent.ConcurrentHashMap 9 | 10 | class KeyUriRouterFactory( 11 | private val logger: UriRouterLogger? = RouterPlugin.logger, 12 | private val keyExtractor: (String) -> String = { it -> 13 | val uri = it.toDeepLinkUri() 14 | "${uri.scheme}${uri.host}" 15 | } 16 | ) : UriRouterFactory { 17 | override fun create(): UriRouter { 18 | return KeyUriRouter(logger, keyExtractor) 19 | } 20 | } 21 | 22 | class SimpleUriRouterFactory( 23 | private val logger: UriRouterLogger? = RouterPlugin.logger, 24 | private val isSupportConcurrent: Boolean = false 25 | ) : UriRouterFactory { 26 | override fun create(): UriRouter { 27 | return SimpleUriRouter( 28 | logger = logger, 29 | dataHolder = if (isSupportConcurrent) { 30 | ConcurrentHashMap() 31 | } else { 32 | mutableMapOf() 33 | } 34 | ) 35 | } 36 | } 37 | 38 | interface UriRouterFactory { 39 | fun create(): UriRouter 40 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/autoregister/AutoRegister.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.autoregister 2 | 3 | @RequiresOptIn(message = "This API is experimental. It doesn't comply with all feature yet.") 4 | @Retention(AnnotationRetention.BINARY) 5 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 6 | annotation class AutoRegister -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/autoregister/RouteAutoRegisterMiddleware.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.autoregister 2 | 3 | import android.content.Context 4 | import nolambda.linkrouter.android.BaseRoute 5 | import nolambda.linkrouter.android.RouteParam 6 | import nolambda.linkrouter.android.RouterPlugin 7 | import nolambda.linkrouter.android.middlewares.MiddleWareResult 8 | import nolambda.linkrouter.android.middlewares.Middleware 9 | 10 | typealias NameResolver = (String) -> String 11 | 12 | @AutoRegister 13 | class RouteAutoRegisterMiddleware( 14 | private val plugin: RouterPlugin = RouterPlugin, 15 | private val nameResolver: NameResolver = { name -> "nolambda.init.route.${name}" } 16 | ) : Middleware { 17 | 18 | companion object { 19 | private const val TAG = "RouteAutoRegister" 20 | } 21 | 22 | override fun onRouting(route: BaseRoute<*>, routeParam: RouteParam<*, Any>): MiddleWareResult { 23 | if (plugin.isUseAnnotationProcessor.not()) return MiddleWareResult(route, routeParam) 24 | val name = "${route.javaClass.simpleName}RouteInit" 25 | val fullClassName = nameResolver(name) 26 | try { 27 | val appContext = plugin.appContext 28 | val routeInit = Class.forName(fullClassName) 29 | val instance = routeInit.getDeclaredConstructor(Context::class.java).newInstance(appContext) 30 | 31 | instance as RouteInit 32 | instance.onInit(appContext) 33 | 34 | } catch (e: ClassNotFoundException) { 35 | plugin.logger?.invoke("No initialization found for $fullClassName") 36 | } 37 | return MiddleWareResult(route, routeParam) 38 | } 39 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/autoregister/RouteInit.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.autoregister 2 | 3 | import android.content.Context 4 | 5 | @AutoRegister 6 | interface RouteInit { 7 | fun onInit(appContext: Context) 8 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/measure/DefaultMeasureConfig.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.measure 2 | 3 | import nolambda.linkrouter.android.RouterPlugin 4 | import kotlin.system.measureNanoTime 5 | import kotlin.system.measureTimeMillis 6 | 7 | class DefaultMeasureConfig : MeasureConfig { 8 | override fun shouldMeasure(name: String): Boolean = true 9 | override fun doMeasure(name: String, operation: () -> T): T { 10 | var returnValue: T 11 | val time = measureNanoTime { 12 | returnValue = operation() 13 | } 14 | RouterPlugin.logger?.invoke("[Measure] [$name] - $time ns") 15 | return returnValue 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/measure/MeasureConfig.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.measure 2 | 3 | interface MeasureConfig { 4 | fun shouldMeasure(name: String): Boolean 5 | fun doMeasure(name: String, operation: () -> T): T 6 | } 7 | -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/measure/MeasureMiddleWare.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.measure 2 | 3 | import nolambda.linkrouter.android.BaseRoute 4 | import nolambda.linkrouter.android.RouteParam 5 | import nolambda.linkrouter.android.middlewares.MiddleWareResult 6 | import nolambda.linkrouter.android.middlewares.Middleware 7 | 8 | class MeasureMiddleWare( 9 | private val measureConfig: MeasureConfig, 10 | private val middleWares: List> 11 | ) : Middleware { 12 | override fun onRouting(route: BaseRoute<*>, routeParam: RouteParam<*, Extra>): MiddleWareResult { 13 | val initial = MiddleWareResult(route, routeParam) 14 | return middleWares.fold(initial) { acc, middleware -> 15 | measureConfig.doMeasure("$middleware") { 16 | middleware.onRouting(acc.route, acc.routeParam) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/measure/MeasuredAbstractAppRouter.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.measure 2 | 3 | import nolambda.linkrouter.android.AbstractAppRouter 4 | import nolambda.linkrouter.android.BaseRoute 5 | import nolambda.linkrouter.android.Route 6 | import nolambda.linkrouter.android.RouteHandler 7 | import nolambda.linkrouter.android.RouteParam 8 | import nolambda.linkrouter.android.RouteWithParam 9 | import nolambda.linkrouter.android.SimpleUriRouterFactory 10 | import nolambda.linkrouter.android.UriRouterFactory 11 | import nolambda.linkrouter.android.middlewares.Middleware 12 | import nolambda.linkrouter.android.registerstrategy.EagerRegisterStrategy 13 | import nolambda.linkrouter.android.registerstrategy.RegisterStrategy 14 | 15 | /** 16 | * This router is intended for measuring actions that router execute 17 | */ 18 | abstract class MeasuredAbstractAppRouter( 19 | private val config: MeasureConfig, 20 | vararg middleWares: Middleware = emptyArray(), 21 | registerStrategy: RegisterStrategy = EagerRegisterStrategy(), 22 | uriRouterFactory: UriRouterFactory = SimpleUriRouterFactory() 23 | ) : AbstractAppRouter( 24 | middleWares = middleWares, 25 | registerStrategy = registerStrategy, 26 | uriRouterFactory = uriRouterFactory 27 | ) { 28 | 29 | override fun createMiddleWares() = measureAndLog("registerAllMiddleWares") { 30 | super.createMiddleWares() 31 | } 32 | 33 | override fun cleanRouter() = measureAndLog("cleanRouter") { 34 | super.cleanRouter() 35 | } 36 | 37 | override fun

register(route: BaseRoute

, handler: RouteHandler) = 38 | measureAndLog("register $route") { 39 | super.register(route, handler) 40 | } 41 | 42 | override fun

push(route: RouteWithParam

, param: P) = measureAndLog("push $route") { 43 | super.push(route, param) 44 | } 45 | 46 | override fun push(route: Route) = measureAndLog("push $route") { 47 | super.push(route) 48 | } 49 | 50 | override fun goTo(uri: String) = measureAndLog("goTo $uri") { 51 | super.goTo(uri) 52 | } 53 | 54 | override fun

applyMiddleware( 55 | route: BaseRoute<*>, 56 | routeParam: RouteParam 57 | ) = measureAndLog("applyMiddleware") { 58 | super.applyMiddleware(route, routeParam) 59 | } 60 | 61 | private fun measureAndLog(name: String, block: () -> T): T { 62 | if (config.shouldMeasure(name).not()) return block() 63 | return config.doMeasure(name, block) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/middlewares/MiddleWareResult.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.middlewares 2 | 3 | import nolambda.linkrouter.android.BaseRoute 4 | import nolambda.linkrouter.android.RouteParam 5 | 6 | data class MiddleWareResult( 7 | val route: BaseRoute<*>, 8 | val routeParam: RouteParam<*, Extra> 9 | ) -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/middlewares/Middleware.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.middlewares 2 | 3 | import nolambda.linkrouter.android.BaseRoute 4 | import nolambda.linkrouter.android.RouteParam 5 | 6 | interface Middleware { 7 | fun onRouting(route: BaseRoute<*>, routeParam: RouteParam<*, Extra>): MiddleWareResult 8 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/registerstrategy/EagerRegisterStrategy.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.registerstrategy 2 | 3 | import nolambda.linkrouter.SimpleRouter 4 | import nolambda.linkrouter.UriRouter 5 | import nolambda.linkrouter.android.BaseRoute 6 | import nolambda.linkrouter.android.RouteHandler 7 | import nolambda.linkrouter.android.UriResult 8 | 9 | class EagerRegisterStrategy : RegisterStrategy { 10 | 11 | override fun

register( 12 | simpleRouter: SimpleRouter>, 13 | uriRouter: UriRouter, 14 | route: BaseRoute

, 15 | handler: RouteHandler 16 | ) { 17 | val paths = route.routePaths 18 | 19 | // Handle non path 20 | simpleRouter.addEntry(route) { 21 | @Suppress("UNCHECKED_CAST") 22 | handler as RouteHandler<*, *, *> 23 | } 24 | 25 | // Handle the path 26 | if (paths.isEmpty()) return 27 | 28 | paths.forEach { path -> 29 | if (path.isBlank()) return@forEach 30 | uriRouter.addEntry(path, matcher = route.pathMatcher()) { uri, param -> 31 | UriResult(uri, route, param) 32 | } 33 | } 34 | } 35 | 36 | override fun await() { 37 | // No operation needed, because all operations is finished on register 38 | } 39 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/registerstrategy/LazyRegisterStrategy.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.registerstrategy 2 | 3 | import nolambda.linkrouter.SimpleRouter 4 | import nolambda.linkrouter.UriRouter 5 | import nolambda.linkrouter.android.BaseRoute 6 | import nolambda.linkrouter.android.RouteHandler 7 | import nolambda.linkrouter.android.UriResult 8 | import java.util.concurrent.ExecutorService 9 | import java.util.concurrent.Executors 10 | import java.util.concurrent.Future 11 | 12 | /** 13 | * Register simple route immediately but lazy register uri routes concurrently. 14 | * For now the only compatible way to use this is to set 15 | * [nolambda.linkrouter.android.SimpleUriRouterFactory.isSupportConcurrent] to true 16 | * 17 | * @see [nolambda.linkrouter.android.SimpleUriRouterFactory] 18 | */ 19 | class LazyRegisterStrategy( 20 | private val executor: ExecutorService = Executors.newFixedThreadPool(2) 21 | ) : RegisterStrategy { 22 | 23 | private val futures = mutableSetOf>() 24 | 25 | override fun

register( 26 | simpleRouter: SimpleRouter>, 27 | uriRouter: UriRouter, 28 | route: BaseRoute

, 29 | handler: RouteHandler 30 | ) { 31 | // Handle non path 32 | simpleRouter.addEntry(route) { 33 | @Suppress("UNCHECKED_CAST") 34 | handler as RouteHandler<*, *, *> 35 | } 36 | 37 | // Handle the path 38 | val paths = route.routePaths 39 | if (paths.isEmpty()) return 40 | 41 | val future = executor.submit { 42 | paths.forEach { path -> 43 | if (path.isBlank()) return@forEach 44 | uriRouter.addEntry(path, matcher = route.pathMatcher()) { uri, param -> 45 | UriResult(uri, route, param) 46 | } 47 | } 48 | } 49 | futures.add(future) 50 | } 51 | 52 | override fun await() { 53 | val clone = futures.toList() 54 | futures.clear() 55 | clone.forEach { 56 | it.get() 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /android/src/main/java/nolambda/linkrouter/android/registerstrategy/RegisterStrategy.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.registerstrategy 2 | 3 | import nolambda.linkrouter.SimpleRouter 4 | import nolambda.linkrouter.UriRouter 5 | import nolambda.linkrouter.android.BaseRoute 6 | import nolambda.linkrouter.android.RouteHandler 7 | import nolambda.linkrouter.android.UriResult 8 | 9 | interface RegisterStrategy { 10 | fun

register( 11 | simpleRouter: SimpleRouter>, 12 | uriRouter: UriRouter, 13 | route: BaseRoute

, 14 | handler: RouteHandler 15 | ) 16 | 17 | fun await() 18 | } 19 | -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | android 3 | 4 | -------------------------------------------------------------------------------- /android/src/test/java/nolambda/linkrouter/android/AndroidPerformanceTest.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | import nolambda.linkrouter.DeepLinkEntry 6 | import nolambda.linkrouter.android.registerstrategy.LazyRegisterStrategy 7 | import kotlin.system.measureTimeMillis 8 | 9 | class AndroidPerformanceTest : StringSpec({ 10 | val eagerRouter = object : AbstractAppRouter() {} 11 | val lazyRouter = object : AbstractAppRouter( 12 | registerStrategy = LazyRegisterStrategy(), 13 | uriRouterFactory = SimpleUriRouterFactory(isSupportConcurrent = true) 14 | ) {} 15 | 16 | val size = 10_000L 17 | 18 | val routes = (0 until size).map { 19 | object : Route("https://test.com/$it") {} 20 | } 21 | 22 | val register = { router: AbstractAppRouter -> 23 | val time = measureTimeMillis { 24 | routes.forEach { route -> 25 | router.register(route) { 26 | val result = route.routePaths.first() 27 | println("Triggered: ${it.info.uri}") 28 | result 29 | } 30 | } 31 | } 32 | println("registers took $time ms for $size entries") 33 | } 34 | 35 | val goTo = { router: AbstractAppRouter -> 36 | val time = measureTimeMillis { 37 | val route = routes.random().routePaths.first() 38 | val result = router.goTo(route) 39 | 40 | println("Go to => $route") 41 | 42 | // Assert 43 | result.isHandled shouldBe true 44 | result.getResultOrNull() shouldBe route 45 | } 46 | println("goTo took $time ms to resolve from $size entries") 47 | } 48 | 49 | "warm up" { 50 | DeepLinkEntry.parse(routes.first().routePaths.first()) 51 | } 52 | 53 | "eager register speed" { 54 | register(eagerRouter) 55 | } 56 | 57 | "eager go to speed" { 58 | goTo(eagerRouter) 59 | } 60 | 61 | "lazy strategy register" { 62 | register(lazyRouter) 63 | } 64 | 65 | "lazy go to speed" { 66 | goTo(lazyRouter) 67 | } 68 | }) 69 | 70 | -------------------------------------------------------------------------------- /android/src/test/java/nolambda/linkrouter/android/DeepLinkSpec.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | import nolambda.linkrouter.DeepLinkUri 6 | import nolambda.linkrouter.android.test.DeepLinkAssertions 7 | import nolambda.linkrouter.android.test.testDeepLink 8 | import java.lang.NumberFormatException 9 | 10 | class DeepLinkSpec : StringSpec({ 11 | 12 | data class IdParam(val id: String) 13 | 14 | val userRoute = object : RouteWithParam("nolambda://user/{id}") { 15 | override fun mapUri(uri: DeepLinkUri, raw: Map): IdParam { 16 | val id = (raw["id"] ?: error("")).toInt() 17 | return IdParam(id.toString()) 18 | } 19 | } 20 | 21 | val itemRoute = object : RouteWithParam("https://nolambda.stream/items/{id}") { 22 | override fun mapUri(uri: DeepLinkUri, raw: Map): IdParam { 23 | return IdParam(raw["id"] ?: error("")) 24 | } 25 | } 26 | 27 | val router = object : AbstractAppRouter() {} 28 | 29 | "test deep link" { 30 | router.testDeepLink(listOf( 31 | userRoute to listOf( 32 | "nolambda://user/1" to DeepLinkAssertions.shouldValid(), 33 | "nolambda://user/abc" to DeepLinkAssertions.shouldThrow() 34 | ), 35 | itemRoute to listOf( 36 | "https://nolambda.stream/items/10" to { it.routeParam?.param?.id shouldBe "10" }, 37 | "http://nolambda.stream/items/10" to DeepLinkAssertions.shouldValid(false) 38 | ) 39 | )) 40 | } 41 | }) -------------------------------------------------------------------------------- /android/src/test/java/nolambda/linkrouter/android/MiddleWareSpec.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | import nolambda.linkrouter.android.middlewares.MiddleWareResult 6 | import nolambda.linkrouter.android.middlewares.Middleware 7 | import nolambda.linkrouter.android.test.TestRoute 8 | import nolambda.linkrouter.android.test.TestRouteWithParam 9 | import nolambda.linkrouter.android.test.testHit 10 | 11 | class MiddleWareSpec : StringSpec({ 12 | val firstRoute = TestRoute() 13 | val secondRoute = TestRoute() 14 | val thirdRoute = TestRouteWithParam() 15 | 16 | var shouldReroute = false 17 | val rerouteMiddleware = object : Middleware { 18 | override fun onRouting(route: BaseRoute<*>, routeParam: RouteParam<*, Unit>): MiddleWareResult { 19 | if (route == thirdRoute) { 20 | return MiddleWareResult(route, routeParam.copyWithParam("")) 21 | } 22 | 23 | val finalRoute = if (shouldReroute) { 24 | firstRoute 25 | } else { 26 | route 27 | } 28 | return MiddleWareResult(finalRoute, routeParam) 29 | } 30 | } 31 | 32 | val testRouter = object : AbstractAppRouter(rerouteMiddleware) {} 33 | 34 | "Middleware should return original route" { 35 | testRouter.testHit(firstRoute) shouldBe true 36 | testRouter.testHit(secondRoute) shouldBe true 37 | } 38 | 39 | "middleware should reroute" { 40 | shouldReroute = true 41 | testRouter.testHit(firstRoute) shouldBe true 42 | testRouter.testHit(secondRoute) shouldBe false 43 | } 44 | 45 | "Middleware should replace the param" { 46 | testRouter.testHit(thirdRoute, "ABC") { 47 | it.param shouldBe "" 48 | true 49 | } 50 | } 51 | 52 | }) -------------------------------------------------------------------------------- /android/src/test/java/nolambda/linkrouter/android/RouterAutoRegisterMiddlewareSpec.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android 2 | 3 | import android.content.Context 4 | import io.kotest.core.spec.style.StringSpec 5 | import io.kotest.matchers.shouldBe 6 | import nolambda.linkrouter.android.autoregister.AutoRegister 7 | import nolambda.linkrouter.android.autoregister.RouteAutoRegisterMiddleware 8 | import nolambda.linkrouter.android.autoregister.RouteInit 9 | import nolambda.linkrouter.android.test.TestRoute 10 | 11 | @OptIn(AutoRegister::class) 12 | class RouterAutoRegisterMiddlewareSpec : StringSpec({ 13 | 14 | val testRouter = object : AbstractAppRouter() {} 15 | 16 | var isResolving = false 17 | val routeAutoRegister = RouteAutoRegisterMiddleware { name -> 18 | isResolving = true 19 | "nolambda.linkrouter.android.${name}" 20 | } 21 | 22 | "It should not register if the annotation is false" { 23 | RouterPlugin.isUseAnnotationProcessor = false 24 | routeAutoRegister.onRouting( 25 | TestRoute(), 26 | RouteParam(param = null, ActionInfo(testRouter)) 27 | ) 28 | 29 | isResolving shouldBe false 30 | } 31 | 32 | // TODO: This is ignored for now because we can't mock the Context 33 | "!It should resolve and invoke init" { 34 | RouterPlugin.isUseAnnotationProcessor = true 35 | routeAutoRegister.onRouting( 36 | TestRoute(), 37 | RouteParam(param = null, ActionInfo(testRouter)) 38 | ) 39 | 40 | isResolving shouldBe true 41 | } 42 | }) 43 | 44 | @OptIn(AutoRegister::class) 45 | class TestRouteRouteInit(private val context: Context) : RouteInit { 46 | override fun onInit(appContext: Context) { 47 | println("On TestRoute init!") 48 | } 49 | } -------------------------------------------------------------------------------- /annotations/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /annotations/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | 4 | dependencies { 5 | implementation fileTree(dir: 'libs', include: ['*.jar']) 6 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 7 | } 8 | 9 | compileKotlin { 10 | kotlinOptions { 11 | jvmTarget = "1.8" 12 | } 13 | } 14 | compileTestKotlin { 15 | kotlinOptions { 16 | jvmTarget = "1.8" 17 | } 18 | } -------------------------------------------------------------------------------- /annotations/src/main/java/nolambda/linkrouter/annotations/Navigate.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.annotations 2 | 3 | import kotlin.reflect.KClass 4 | 5 | @Target(AnnotationTarget.CLASS) 6 | @Retention(AnnotationRetention.SOURCE) 7 | annotation class Navigate( 8 | val route: KClass<*> 9 | ) -------------------------------------------------------------------------------- /art/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esafirm/universal-router/9fa8b8a1e0f7d728e4b229105b1a7ceeb0d700b0/art/diagram.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | apply from: "${rootDir}/buildSystem/stdlib.gradle" 5 | apply from: "${rootDir}/buildSystem/dependencies.gradle" 6 | 7 | ext.kotlin_version = '1.5.31' 8 | repositories { 9 | google() 10 | mavenCentral() 11 | } 12 | dependencies { 13 | classpath 'com.android.tools.build:gradle:7.0.3' 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | } 16 | } 17 | 18 | ext { 19 | versions = [ 20 | compileSdk: 30, 21 | minSdk : 19 22 | ] 23 | } 24 | 25 | allprojects { 26 | group = 'universal.router' 27 | 28 | repositories { 29 | google() 30 | mavenCentral() 31 | } 32 | } 33 | 34 | task clean(type: Delete) { 35 | delete rootProject.buildDir 36 | } 37 | -------------------------------------------------------------------------------- /buildSystem/base.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion versions.compileSdk 6 | 7 | defaultConfig { 8 | minSdkVersion versions.minSdk 9 | targetSdkVersion versions.compileSdk 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles 'consumer-rules.pro' 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | 29 | testOptions { 30 | unitTests.all { 31 | useJUnitPlatform() 32 | } 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = "1.8" 37 | } 38 | } 39 | 40 | task sourcesJar(type: Jar) { 41 | archiveClassifier.set('sources') 42 | from android.sourceSets.main.java.srcDirs 43 | } 44 | 45 | artifacts { 46 | archives sourcesJar 47 | } 48 | 49 | dependencies { 50 | implementation fileTree(dir: 'libs', include: ['*.jar']) 51 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 52 | } 53 | -------------------------------------------------------------------------------- /buildSystem/dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | final appCompatVersion = '1.2.0' 3 | final kotestVersion = "4.2.6" 4 | final coroutineVersion = '1.5.1' 5 | 6 | deps = [ 7 | appCompat: "androidx.appcompat:appcompat:$appCompatVersion", 8 | coroutine: "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" 9 | ] 10 | 11 | testDeps = [ 12 | "io.mockk:mockk:1.10.0", 13 | "io.kotest:kotest-runner-junit5:$kotestVersion", 14 | "io.kotest:kotest-assertions-core:$kotestVersion" 15 | ] 16 | } -------------------------------------------------------------------------------- /buildSystem/stdlib.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | buildSystemDir = "${rootDir}/buildSystem" 3 | rootBuildDir = "${rootDir}/build" 4 | 5 | appVersion = '2.9.0' 6 | appGroupId = 'com.github.esafirm.universal-router' 7 | } -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /core/.kotlintest/spec_failures: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esafirm/universal-router/9fa8b8a1e0f7d728e4b229105b1a7ceeb0d700b0/core/.kotlintest/spec_failures -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'maven-publish' 4 | id 'kotlin' 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | final JVM_TARGET = '1.8' 12 | 13 | compileKotlin { 14 | kotlinOptions { 15 | jvmTarget = JVM_TARGET 16 | } 17 | } 18 | compileTestKotlin { 19 | kotlinOptions { 20 | jvmTarget = JVM_TARGET 21 | } 22 | } 23 | 24 | task sourcesJar(type: Jar) { 25 | archiveClassifier.set('sources') 26 | from sourceSets.main.allSource 27 | } 28 | 29 | java { 30 | sourceCompatibility = JavaVersion.VERSION_1_8 31 | targetCompatibility = JavaVersion.VERSION_1_8 32 | 33 | withSourcesJar() 34 | } 35 | 36 | afterEvaluate { 37 | publishing { 38 | publications { 39 | release(MavenPublication) { 40 | from components.java 41 | 42 | groupId = appGroupId 43 | artifactId = project.name 44 | version = appVersion 45 | } 46 | } 47 | } 48 | } 49 | 50 | /* --------------------------------------------------- */ 51 | /* > Dependencies */ 52 | /* --------------------------------------------------- */ 53 | 54 | dependencies { 55 | implementation fileTree(dir: 'libs', include: ['*.jar']) 56 | 57 | implementation 'com.squareup.okio:okio:2.10.0' 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 59 | implementation deps.coroutine 60 | 61 | final kotestVersion = "4.6.1" 62 | testImplementation "io.kotest:kotest-runner-junit5:$kotestVersion" 63 | testImplementation "io.kotest:kotest-assertions-core:$kotestVersion" 64 | } 65 | -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/DeepLinkEntry.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri 4 | import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUriOrNull 5 | import java.util.regex.Pattern 6 | 7 | class DeepLinkEntry private constructor( 8 | val uri: DeepLinkUri, 9 | private val regex: Pattern, 10 | private val parameters: Set 11 | ) { 12 | 13 | companion object { 14 | private const val PARAM = "([a-zA-Z][a-zA-Z0-9_-]*)" 15 | private const val PARAM_REGEX = "%7B($PARAM)%7D" 16 | private const val PARAM_VALUE = "([a-zA-Z0-9_#'!+%~,\\-\\.\\@\\$\\:]+)" 17 | 18 | private val PARAM_PATTERN = Pattern.compile(PARAM_REGEX) 19 | 20 | fun parse(url: String): DeepLinkEntry { 21 | return parse(url.toDeepLinkUri()) 22 | } 23 | 24 | fun parse(deepLinkUri: DeepLinkUri): DeepLinkEntry { 25 | val schemeHostAndPath = schemeHostAndPath(deepLinkUri) 26 | val regex = Pattern.compile(schemeHostAndPath.replace(PARAM_REGEX.toRegex(), PARAM_VALUE) + "$") 27 | return DeepLinkEntry(deepLinkUri, regex, parseParameters(deepLinkUri)) 28 | } 29 | 30 | private fun schemeHostAndPath(uri: DeepLinkUri): String { 31 | return uri.scheme + "://" + uri.host + uri.encodedPath 32 | } 33 | 34 | private fun parseParameters(uri: DeepLinkUri): Set { 35 | val matcher = PARAM_PATTERN.matcher(uri.host + uri.encodedPath) 36 | val patterns = linkedSetOf() 37 | while (matcher.find()) { 38 | patterns.add(matcher.group(1)) 39 | } 40 | return patterns 41 | } 42 | } 43 | 44 | fun matches(inputUri: String): Boolean { 45 | val deepLinkUri = inputUri.toDeepLinkUriOrNull() 46 | return deepLinkUri != null && regex.matcher(schemeHostAndPath(deepLinkUri)).find() 47 | } 48 | 49 | fun getParameters(inputUri: String): Map { 50 | return getParameters(inputUri.toDeepLinkUri()) 51 | } 52 | 53 | fun getParameters(deepLinkUri: DeepLinkUri): Map { 54 | val matcher = regex.matcher(schemeHostAndPath(deepLinkUri)) 55 | val paramsMap = mutableMapOf() 56 | 57 | var i = 1 58 | if (matcher.matches()) { 59 | parameters.forEach { key -> 60 | val value = matcher.group(i++) 61 | if (value != null && "" != value.trim { it <= ' ' }) { 62 | paramsMap[key] = value 63 | } 64 | } 65 | } 66 | return paramsMap 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/KeyUriRouter.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | import nolambda.linkrouter.matcher.UriMatcher 4 | 5 | /** 6 | * router that use "key" to group [DeepLinkUri] or [DeepLinkEntry] for faster 7 | * register and lookup 8 | * 9 | * Flow: 10 | * 1. Register the route to [handlerMap] and [keyToUriMap] 11 | * 2. When resolving, it first search in [keyToEntryMap] 12 | * a. If no match, then search to [keyToUriMap], this will producing [DeepLinkEntry] and add it to [keyToEntryMap] 13 | * b. If match it will resulting the registered route 14 | * 3. Get the handler from [handlerMap] by using the registered route 15 | * 16 | * Please note, this router addition process is not thread-safe. 17 | */ 18 | class KeyUriRouter( 19 | private val logger: UriRouterLogger? = null, 20 | private val keyExtractor: (String) -> String 21 | ) : UriRouter(logger) { 22 | 23 | /** 24 | * Key: From key extractor. Usually scheme + host + path 25 | * Value: Set of Pair of [DeepLinkEntry] and the matcher [UriMatcher] 26 | * 27 | * The result from here will be moved to [keyToEntryMap] 28 | * and the entry will be deleted after that 29 | */ 30 | private val keyToUriMap = mutableMapOf>>() 31 | 32 | /** 33 | * Key: From key extractor. Usually scheme + host + path 34 | * Value: Set of Pair of [DeepLinkEntry] and the matcher [UriMatcher] 35 | * 36 | * If there's a value found in here, we don't need to search in [keyToUriMap] 37 | */ 38 | private val keyToEntryMap = 39 | mutableMapOf>>() 40 | 41 | /** 42 | * Key: Registered route 43 | * Value: Registered lambda 44 | * 45 | * If we found registered route (key) from [keyToEntryMap] or [keyToUriMap] 46 | * we will find the registered lambda in here 47 | */ 48 | private val handlerMap = mutableMapOf>() 49 | 50 | override fun clear() { 51 | keyToUriMap.clear() 52 | } 53 | 54 | override fun resolveEntry(route: String): Pair>? { 55 | val key = keyExtractor(route) 56 | 57 | logger?.run { 58 | invoke("Key: $key") 59 | invoke("Entries map size: ${keyToEntryMap.size}") 60 | invoke("Uri map size: ${keyToUriMap.size}") 61 | } 62 | 63 | val result = getResultFromEntryMap(key, route) 64 | return if (result == null) { 65 | logger?.invoke("Get from uri map for key: $key") 66 | getResultFromUriMap(key, route) 67 | } else result 68 | } 69 | 70 | private fun getResultFromUriMap( 71 | key: String, 72 | route: String 73 | ): Pair>? { 74 | 75 | var entry: DeepLinkEntry? = null 76 | var matcher: UriMatcher? = null 77 | var pairOfUriAndMatcher: Pair? = null 78 | 79 | val uriList = keyToUriMap[key] ?: return null 80 | val actualKey = uriList.firstOrNull { pair -> 81 | 82 | val currentEntry = DeepLinkEntry.parse(pair.first) 83 | val currentMatcher = pair.second 84 | 85 | // Pass the data to outside variable, so we can use it for other case 86 | pairOfUriAndMatcher = pair 87 | entry = currentEntry 88 | matcher = currentMatcher 89 | 90 | // Add parsed entry to entry map 91 | val sets = keyToEntryMap.getOrPut(key) { mutableSetOf() } 92 | sets.add(Triple(currentEntry, pair.first, currentMatcher)) 93 | 94 | currentMatcher.match(currentEntry, route) 95 | } ?: return null 96 | 97 | /** 98 | * Because it's already available in [keyToEntryMap] remove the old data in [keyToUriMap] 99 | */ 100 | val sets = keyToUriMap[key] 101 | if (sets == null || sets.size <= 1) { 102 | keyToUriMap.remove(key) 103 | } else { 104 | sets.remove(pairOfUriAndMatcher) 105 | } 106 | 107 | return createResult(actualKey.first, entry, matcher) 108 | } 109 | 110 | private fun getResultFromEntryMap( 111 | key: String, 112 | route: String 113 | ): Pair>? { 114 | val set = keyToEntryMap[key] ?: return null 115 | val actualKey = set.firstOrNull { (entry, _, matcher) -> 116 | matcher.match(entry, route) 117 | } ?: return null 118 | 119 | return createResult(actualKey.second, actualKey.first, actualKey.third) 120 | } 121 | 122 | private fun createResult( 123 | registeredRoute: String, 124 | entry: DeepLinkEntry?, 125 | matcher: UriMatcher? 126 | ): Pair> { 127 | val handler = handlerMap[registeredRoute] 128 | ?: error("Handler not available for $registeredRoute") 129 | 130 | return Pair( 131 | first = entry ?: error("Entry not available fro $registeredRoute"), 132 | second = EntryValue( 133 | handler, 134 | matcher ?: error("Matcher not available fro $registeredRoute") 135 | ) 136 | ) 137 | } 138 | 139 | /** 140 | * This is not thread-safe 141 | */ 142 | override fun addEntry( 143 | vararg uris: String, 144 | matcher: UriMatcher, 145 | handler: UriRouterHandler 146 | ) { 147 | uris.forEach { route -> 148 | val key = keyExtractor(route) 149 | inputToEntryContainer(key, route, matcher) 150 | handlerMap[route] = handler 151 | } 152 | } 153 | 154 | private fun inputToEntryContainer(key: String, registeredRoute: String, matcher: UriMatcher) { 155 | val entriesHolder = keyToUriMap.getOrPut(key) { mutableSetOf() } 156 | entriesHolder.add(registeredRoute to matcher) 157 | } 158 | } -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/MapOpt.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | fun Map.optString(key: String, defaultValue: String = ""): String = 4 | if (containsKey(key)) { 5 | get(key) ?: defaultValue 6 | } else defaultValue 7 | 8 | fun Map.optLong(key: String, defaultValue: Long = 0L): Long = 9 | if (containsKey(key)) { 10 | try { 11 | get(key)?.toLong() ?: defaultValue 12 | } catch (ex: Exception) { 13 | defaultValue 14 | } 15 | } else defaultValue 16 | 17 | fun Map.optInt(key: String, defaultValue: Int = 0): Int = 18 | if (containsKey(key)) { 19 | try { 20 | get(key)?.toInt() ?: defaultValue 21 | } catch (ex: Exception) { 22 | defaultValue 23 | } 24 | } else defaultValue 25 | 26 | fun Map.optFloat(key: String, defaultValue: Float = 0.0F): Float = 27 | if (containsKey(key)) { 28 | try { 29 | get(key)?.toFloat() ?: defaultValue 30 | } catch (ex: Exception) { 31 | defaultValue 32 | } 33 | } else defaultValue 34 | 35 | fun Map.optDouble(key: String, defaultValue: Double = 0.0): Double = 36 | if (containsKey(key)) { 37 | try { 38 | get(key)?.toDouble() ?: defaultValue 39 | } catch (ex: Exception) { 40 | defaultValue 41 | } 42 | } else defaultValue 43 | 44 | fun Map.optBoolean(key: String, defaultValue: Boolean = false): Boolean = 45 | if (containsKey(key)) { 46 | val value = get(key) 47 | value.equals("true", true) && value.equals("1", true) 48 | } else defaultValue 49 | -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/RoutePath.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | open class RoutePath( 4 | private vararg val basePaths: String 5 | ) { 6 | fun paths(vararg items: String): Array { 7 | val paths = mutableListOf() 8 | basePaths.forEach { base -> 9 | items.map { item -> 10 | paths.add(base + item) 11 | } 12 | } 13 | return paths.toTypedArray() 14 | } 15 | 16 | operator fun plus(path: RoutePath): RoutePath { 17 | val combinedBasePaths = arrayOf(*this.basePaths, *path.basePaths) 18 | return RoutePath(*combinedBasePaths) 19 | } 20 | 21 | fun wrap(path: RoutePath): RoutePath { 22 | val wrappedBasePath = paths(*path.basePaths) 23 | return RoutePath(*wrappedBasePath) 24 | } 25 | } -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/SimpleRouter.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | import nolambda.linkrouter.error.RouteNotFoundException 4 | 5 | internal interface Router { 6 | fun clear() 7 | fun resolve(route: REQ): RES 8 | } 9 | 10 | typealias RouterHandler = (Any) -> T 11 | 12 | abstract class SimpleRouter : Router { 13 | 14 | private var entries = linkedMapOf>() 15 | 16 | fun addEntry(route: Any, handler: RouterHandler) { 17 | entries[route] = handler 18 | } 19 | 20 | override fun clear() { 21 | entries.clear() 22 | } 23 | 24 | override fun resolve(route: Any): RES { 25 | val entry = entries[route] ?: throw RouteNotFoundException(route) 26 | return entry.invoke(route) 27 | } 28 | } -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/SimpleUriRouter.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | import nolambda.linkrouter.matcher.UriMatcher 4 | 5 | class SimpleUriRouter( 6 | private val logger: UriRouterLogger? = null, 7 | dataHolder: MutableMap> = mutableMapOf() 8 | ) : UriRouter(logger) { 9 | 10 | private val entries: MutableMap> = dataHolder 11 | 12 | override fun resolveEntry(route: String): Pair>? { 13 | logger?.invoke("Entries size: ${entries.size}") 14 | 15 | return entries.asSequence().firstOrNull { entry -> 16 | val value = entry.value 17 | value.matcher.match(entry.key, route) 18 | }?.toPair() 19 | } 20 | 21 | override fun addEntry(vararg uris: String, matcher: UriMatcher, handler: UriRouterHandler) { 22 | val deepLinkEntries = uris.map { DeepLinkEntry.parse(it) } 23 | deepLinkEntries.forEach { entry -> 24 | entries[entry] = EntryValue(handler, matcher) 25 | } 26 | } 27 | 28 | override fun clear() { 29 | entries.clear() 30 | } 31 | } -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/UriRouter.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri 4 | import nolambda.linkrouter.matcher.DeepLinkEntryMatcher 5 | import nolambda.linkrouter.matcher.UriMatcher 6 | import java.util.concurrent.ConcurrentHashMap 7 | 8 | typealias UriRouterLogger = (String) -> Unit 9 | typealias UriRouterHandler = (DeepLinkUri, Map) -> T 10 | 11 | class EntryValue( 12 | val handler: UriRouterHandler, 13 | val matcher: UriMatcher 14 | ) 15 | 16 | abstract class UriRouter( 17 | private val logger: UriRouterLogger? 18 | ) : Router { 19 | 20 | abstract fun resolveEntry(route: String): Pair>? 21 | 22 | override fun resolve(route: String): RES? { 23 | val result = resolveEntry(route) 24 | if (result == null) { 25 | logger?.invoke("Path not implemented $route") 26 | return null 27 | } 28 | 29 | val deepLinkEntry = result.first 30 | val value = result.second 31 | 32 | val deepLinkUri = route.toDeepLinkUri() 33 | val parameters = deepLinkEntry.getParameters(deepLinkUri) 34 | 35 | return value.handler.invoke(deepLinkUri, parameters) 36 | } 37 | 38 | abstract fun addEntry( 39 | vararg uris: String, 40 | matcher: UriMatcher = DeepLinkEntryMatcher, 41 | handler: UriRouterHandler 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/Util.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Util") 2 | 3 | package nolambda.linkrouter 4 | 5 | /* 6 | * Copyright (C) 2012 The Android Open Source Project 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * Increments [startIndex] until this string is not ASCII whitespace. Stops at [endIndex]. 23 | */ 24 | fun String.indexOfFirstNonAsciiWhitespace(startIndex: Int = 0, endIndex: Int = length): Int { 25 | for (i in startIndex until endIndex) { 26 | when (this[i]) { 27 | '\t', '\n', '\u000C', '\r', ' ' -> Unit 28 | else -> return i 29 | } 30 | } 31 | return endIndex 32 | } 33 | 34 | /** 35 | * Decrements [endIndex] until `input[endIndex - 1]` is not ASCII whitespace. Stops at [startIndex]. 36 | */ 37 | fun String.indexOfLastNonAsciiWhitespace(startIndex: Int = 0, endIndex: Int = length): Int { 38 | for (i in endIndex - 1 downTo startIndex) { 39 | when (this[i]) { 40 | '\t', '\n', '\u000C', '\r', ' ' -> Unit 41 | else -> return i + 1 42 | } 43 | } 44 | return startIndex 45 | } 46 | 47 | /** 48 | * Returns the index of the first character in this string that contains a character in 49 | * [delimiters]. Returns endIndex if there is no such character. 50 | */ 51 | fun String.delimiterOffset(delimiters: String, startIndex: Int = 0, endIndex: Int = length): Int { 52 | for (i in startIndex until endIndex) { 53 | if (this[i] in delimiters) return i 54 | } 55 | return endIndex 56 | } 57 | 58 | /** 59 | * Returns the index of the first character in this string that is [delimiter]. Returns [endIndex] 60 | * if there is no such character. 61 | */ 62 | fun String.delimiterOffset(delimiter: Char, startIndex: Int = 0, endIndex: Int = length): Int { 63 | for (i in startIndex until endIndex) { 64 | if (this[i] == delimiter) return i 65 | } 66 | return endIndex 67 | } 68 | 69 | fun Char.parseHexDigit(): Int = when (this) { 70 | in '0'..'9' -> this - '0' 71 | in 'a'..'f' -> this - 'a' + 10 72 | in 'A'..'F' -> this - 'A' + 10 73 | else -> -1 74 | } 75 | 76 | infix fun Byte.and(mask: Int): Int = toInt() and mask 77 | infix fun Short.and(mask: Int): Int = toInt() and mask 78 | infix fun Int.and(mask: Long): Long = toLong() and mask 79 | -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/error/RouteNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.error 2 | 3 | class RouteNotFoundException(val route: Any) : Exception("No entry for parameter $route") -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/matcher/DeepLinkEntryMatcher.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.matcher 2 | 3 | import nolambda.linkrouter.DeepLinkEntry 4 | 5 | object DeepLinkEntryMatcher : UriMatcher { 6 | override fun match(entry: DeepLinkEntry, url: String): Boolean { 7 | return entry.matches(url) 8 | } 9 | } -------------------------------------------------------------------------------- /core/src/main/java/nolambda/linkrouter/matcher/UriMatcher.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.matcher 2 | 3 | import nolambda.linkrouter.DeepLinkEntry 4 | 5 | interface UriMatcher { 6 | fun match(entry: DeepLinkEntry, url: String): Boolean 7 | } -------------------------------------------------------------------------------- /core/src/test/java/nolambda/linkrouter/DeepLinkUriSpec.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class DeepLinkUriSpec : StringSpec({ 7 | 8 | "Should parse properly" { 9 | val deeplink = DeepLinkEntry.parse("http://something.js/{a}/{b}") 10 | val parameters = deeplink.getParameters("http://something.js/test/123") 11 | 12 | parameters.size shouldBe 2 13 | parameters["a"] shouldBe "test" 14 | parameters["b"] shouldBe "123" 15 | } 16 | 17 | "Should no crash on no parameters" { 18 | val deeplink = DeepLinkEntry.parse("nolamnda://test/{asd}") 19 | val parametes = deeplink.getParameters("nolambda://test") 20 | 21 | parametes.size shouldBe 0 22 | } 23 | 24 | "It should handle slash properly" { 25 | val deeplink = DeepLinkEntry.parse("nolambda://test/{a}/{b}") 26 | val parameters = deeplink.getParameters("nolambda://test/a/b?aaaa=1") 27 | 28 | parameters.size shouldBe 2 29 | parameters["a"] shouldBe "a" 30 | parameters["b"] shouldBe "b" 31 | } 32 | }) 33 | 34 | 35 | -------------------------------------------------------------------------------- /core/src/test/java/nolambda/linkrouter/PerformanceTest.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri 5 | import java.util.* 6 | import kotlin.system.measureTimeMillis 7 | 8 | class PerformanceTest : StringSpec({ 9 | 10 | val size = 20_000L 11 | val random = Random(size) 12 | 13 | val logger = { log: String -> println(log) } 14 | 15 | val simpleRouter = SimpleUriRouter(logger) 16 | val keyRouter = KeyUriRouter(logger) { 17 | val deepLinkUri = it.toDeepLinkUri() 18 | "${deepLinkUri.scheme}${deepLinkUri.host}${deepLinkUri.pathSegments.size}" 19 | } 20 | 21 | val generateEntry = { 22 | val domain = random.nextInt().toString() 23 | val simpleHttp = "http://${domain}.js/{kupon}/{customer_id}" 24 | val simpleHttps = "https://${domain}.js/{kupon}/{customer_id}" 25 | val constantDomain = "http://test.com/${domain}" 26 | arrayOf(simpleHttp, simpleHttps, constantDomain) 27 | } 28 | 29 | val addEntry = { router: UriRouter, entries: Array -> 30 | router.addEntry(*entries) { _, _ -> 31 | print("Resolved!") 32 | } 33 | } 34 | 35 | val entries = (0 until size).map { generateEntry() } 36 | val testEntry = entries[entries.size / 2] 37 | 38 | "warm up" { 39 | val time = measureTimeMillis { 40 | addEntry(simpleRouter, generateEntry()) 41 | addEntry(keyRouter, generateEntry()) 42 | } 43 | println("warm up took $time ms") 44 | } 45 | 46 | "add entries using map" { 47 | val time = measureTimeMillis { 48 | entries.forEach { addEntry(simpleRouter, it) } 49 | } 50 | println("sync takes $time ms to add") 51 | } 52 | 53 | "resolve time" { 54 | val resolveTime = measureTimeMillis { 55 | simpleRouter.resolve(testEntry.first()) 56 | } 57 | println("sync resolve takes $resolveTime ms to resolve") 58 | } 59 | 60 | "add entries using container" { 61 | val time = measureTimeMillis { 62 | entries.forEach { addEntry(keyRouter, it) } 63 | } 64 | println("container takes $time ms to add") 65 | } 66 | 67 | "resolve time container" { 68 | val resolveTime = measureTimeMillis { 69 | keyRouter.resolve(testEntry.first()) 70 | } 71 | println("container resolve takes $resolveTime ms to resolve") 72 | } 73 | }) 74 | 75 | -------------------------------------------------------------------------------- /core/src/test/java/nolambda/linkrouter/RoutePathSpec.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class RoutePathSpec : StringSpec({ 7 | 8 | "it should provide simple path" { 9 | val path = RoutePath("app://test.com", "https://facebook.com") 10 | val result = path.paths("/cart") 11 | 12 | result shouldBe arrayOf("app://test.com/cart", "https://facebook.com/cart") 13 | } 14 | 15 | "it should combine base paths" { 16 | val firstPath = RoutePath("app://test.com") 17 | val secondPath = RoutePath("https://facebook.com") 18 | val combinedPath = firstPath + secondPath 19 | 20 | val result = combinedPath.paths("/cart/{id}") 21 | 22 | result shouldBe arrayOf("app://test.com/cart/{id}", "https://facebook.com/cart/{id}") 23 | } 24 | 25 | "it should wrap the route" { 26 | val webProtocol = RoutePath("http://", "https://") 27 | val webLink = webProtocol.wrap(RoutePath( 28 | "m.bukatoko.com", 29 | "www.bukatoko.com", 30 | )) 31 | 32 | val appProtocol = RoutePath("app://") 33 | val appLink = appProtocol.wrap(RoutePath( 34 | "bukatoko", 35 | )) 36 | 37 | val allLink = webLink + appLink 38 | val result = allLink.paths("/cart") 39 | 40 | result.size shouldBe 5 41 | result shouldBe arrayOf( 42 | "http://m.bukatoko.com/cart", 43 | "http://www.bukatoko.com/cart", 44 | "https://m.bukatoko.com/cart", 45 | "https://www.bukatoko.com/cart", 46 | "app://bukatoko/cart" 47 | ) 48 | } 49 | }) -------------------------------------------------------------------------------- /core/src/test/java/nolambda/linkrouter/SampleDeeplinkTest.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class StringRouterSpec : StringSpec({ 7 | 8 | val stringRouter = SimpleUriRouter() 9 | 10 | stringRouter.addEntry("nolambda://test/{a}/{b}", "https://test/{a}/{b}") { _, param -> 11 | val first = param["a"] 12 | val second = param["b"] 13 | "$second came to the wrong neighborhood $first" 14 | } 15 | 16 | stringRouter.addEntry("http://test.com/{a}") { uri, param -> 17 | val trackId = uri.queryParameter("trackId") 18 | "Track ID for ${param["a"]} is $trackId" 19 | } 20 | 21 | "should return and print valid text" { 22 | stringRouter.resolve("nolambda://test/bro/you") shouldBe "you came to the wrong neighborhood bro" 23 | stringRouter.resolve("https://test/bro/you") shouldBe "you came to the wrong neighborhood bro" 24 | } 25 | 26 | "it should return path and query" { 27 | stringRouter.resolve("http://test.com/featureone?trackId=123") shouldBe "Track ID for featureone is 123" 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /core/src/test/java/nolambda/linkrouter/UriRouterSpec.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri 6 | import nolambda.linkrouter.matcher.DeepLinkEntryMatcher 7 | import nolambda.linkrouter.matcher.UriMatcher 8 | 9 | class UriRouterSpec : StringSpec({ 10 | 11 | val routers = listOf( 12 | SimpleUriRouter(), 13 | KeyUriRouter { 14 | val uri = it.toDeepLinkUri() 15 | "${uri.scheme}${uri.host}" 16 | } 17 | ) 18 | 19 | "it should match and resolve to respected path" { 20 | routers.forEach { router -> 21 | val pathMap = mapOf( 22 | "http://test.com/promo" to "1", 23 | "http://test.com/promo-list" to "2", 24 | "http://test.com/promo-list/promo/" to "3", 25 | "http://test.com/promo-list/item1" to "4", 26 | "http://test.com/promo-list/item2" to "5", 27 | "http://test.com/promo-list/item3" to "6", 28 | "http://test.com/promo-list/item4" to "7", 29 | "http://test.com/promo-list/{a}" to "8", 30 | "http://test.com/promo-list/item5" to "8", // Should hit the above URI 31 | ) 32 | 33 | println("Using $router") 34 | 35 | pathMap.keys.forEach { key -> 36 | router.addEntry(key) { _, param -> 37 | println("Param: $param") 38 | pathMap[key] ?: error("") 39 | } 40 | } 41 | 42 | pathMap.keys.forEach { key -> 43 | val resolved = router.resolve(key) 44 | 45 | println("Expected -> $key : ${pathMap[key]}") 46 | println("Actual -> $key : $resolved") 47 | println("------") 48 | 49 | resolved shouldBe pathMap[key] 50 | } 51 | } 52 | } 53 | 54 | "it should match normal regex" { 55 | routers.forEach { router -> 56 | router.addEntry("http://something.com/.*/{a}") { _, param -> param["a"].orEmpty() } 57 | val result = router.resolve("http://something.com/aaaa/true") 58 | 59 | result shouldBe "true" 60 | } 61 | } 62 | 63 | "it should ignore extra path in url" { 64 | routers.forEach { router -> 65 | router.addEntry("https://example.com/test/{page}/extra") { _, _ -> "" } 66 | val result = router.resolve("https://example.com/test/2") 67 | 68 | result shouldBe null 69 | } 70 | } 71 | 72 | "it should match url with extra query parameter" { 73 | routers.forEach { router -> 74 | val expectedResult = "hi!" 75 | router.addEntry("https://example.com/test/{page}") { _, _ -> expectedResult } 76 | val result = router.resolve("https://example.com/test/2?utm_source=facebook.com") 77 | 78 | result shouldBe expectedResult 79 | } 80 | } 81 | 82 | "it should match custom matcher" { 83 | routers.forEach { router -> 84 | val expectedResult = "123" 85 | router.addEntry("https://test.com?show=true", matcher = object : UriMatcher { 86 | override fun match(entry: DeepLinkEntry, url: String): Boolean { 87 | return DeepLinkEntryMatcher.match(entry, url) && url.toDeepLinkUri() 88 | .queryParameter("show") == "true" 89 | } 90 | }) { _, _ -> expectedResult } 91 | 92 | 93 | router.resolve("https://test.com?show=false") shouldBe null 94 | router.resolve("https://test.com?show=true") shouldBe expectedResult 95 | } 96 | } 97 | }) -------------------------------------------------------------------------------- /extras/android-activityresult/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /extras/android-activityresult/build.gradle: -------------------------------------------------------------------------------- 1 | apply from: "${buildSystemDir}/base.gradle" 2 | apply plugin: 'maven-publish' 3 | 4 | dependencies { 5 | implementation project(':android') 6 | 7 | implementation 'androidx.activity:activity-ktx:1.2.0' 8 | implementation 'androidx.fragment:fragment-ktx:1.3.6' 9 | } 10 | 11 | afterEvaluate { 12 | publishing { 13 | publications { 14 | release(MavenPublication) { 15 | from components.release 16 | 17 | groupId = appGroupId 18 | artifactId = project.name 19 | version = appVersion 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /extras/android-activityresult/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esafirm/universal-router/9fa8b8a1e0f7d728e4b229105b1a7ceeb0d700b0/extras/android-activityresult/consumer-rules.pro -------------------------------------------------------------------------------- /extras/android-activityresult/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/AcitivtyResultRouterExt.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.activity.result.ActivityResult 5 | import androidx.fragment.app.Fragment 6 | import nolambda.linkrouter.android.Route 7 | import nolambda.linkrouter.android.RouteWithParam 8 | import nolambda.linkrouter.android.RouterProcessor 9 | 10 | typealias RouteResultLauncher = (RouterProcessor<*>) -> Unit 11 | typealias RouteResultLauncherWithParam

= (RouterProcessor<*>, param: P) -> Unit 12 | 13 | fun ComponentActivity.registerRouteForResult( 14 | route: Route, 15 | onCallback: (ActivityResult) -> Unit 16 | ): RouteResultLauncher { 17 | val launcher = createLauncher(onCallback) 18 | return { it.pushToProcessor(launcher, route) } 19 | } 20 | 21 | fun

ComponentActivity.registerRouteForResult( 22 | route: RouteWithParam

, 23 | onCallback: (ActivityResult) -> Unit 24 | ): RouteResultLauncherWithParam

{ 25 | val launcher = createLauncher(onCallback) 26 | return { router, param -> router.pushToProcessor(launcher, route, param) } 27 | } 28 | 29 | fun Fragment.registerRouteForResult( 30 | route: Route, 31 | onCallback: (ActivityResult) -> Unit 32 | ): RouteResultLauncher { 33 | val launcher = createLauncher(onCallback) 34 | return { it.pushToProcessor(launcher, route) } 35 | } 36 | 37 | fun

Fragment.registerRouteForResult( 38 | route: RouteWithParam

, 39 | onCallback: (ActivityResult) -> Unit 40 | ): RouteResultLauncherWithParam

{ 41 | val launcher = createLauncher(onCallback) 42 | return { router, param -> router.pushToProcessor(launcher, route, param) } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/ActivityResultLauncherMiddleWare.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller 2 | 3 | import android.content.Intent 4 | import androidx.activity.result.ActivityResultLauncher 5 | import nolambda.linkrouter.android.BaseRoute 6 | import nolambda.linkrouter.android.RouteParam 7 | import nolambda.linkrouter.android.middlewares.MiddleWareResult 8 | import nolambda.linkrouter.android.middlewares.Middleware 9 | 10 | class ActivityResultLauncherMiddleWare( 11 | private val onCreateExtra: (Extra?, ActivityResultLauncher) -> Extra 12 | ) : Middleware { 13 | override fun onRouting(route: BaseRoute<*>, routeParam: RouteParam<*, Extra>): MiddleWareResult { 14 | val navParam = routeParam.param 15 | if (navParam is ActivityResultLauncherProcessor.Param) { 16 | return MiddleWareResult(navParam.originalRoute, routeParam.copyWithParam( 17 | passedParam = navParam.originalParam, 18 | passedExtra = onCreateExtra(routeParam.extra, navParam.launcher) 19 | )) 20 | } 21 | 22 | return MiddleWareResult(route, routeParam) 23 | } 24 | } -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/ActivityResultLauncherProcessor.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller 2 | 3 | import android.content.Intent 4 | import androidx.activity.result.ActivityResultLauncher 5 | import nolambda.linkrouter.android.BaseRoute 6 | import nolambda.linkrouter.android.RouteWithParam 7 | 8 | object ActivityResultLauncherProcessor : RouteWithParam() { 9 | data class Param( 10 | val launcher: ActivityResultLauncher, 11 | val originalParam: Any?, 12 | val originalRoute: BaseRoute<*> 13 | ) 14 | } -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/Helper.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller 2 | 3 | import android.content.Intent 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.result.ActivityResult 6 | import androidx.activity.result.ActivityResultLauncher 7 | import androidx.activity.result.contract.ActivityResultContracts 8 | import androidx.fragment.app.Fragment 9 | import nolambda.linkrouter.android.BaseRoute 10 | import nolambda.linkrouter.android.RouterProcessor 11 | 12 | /* --------------------------------------------------- */ 13 | /* > Launcher Creation */ 14 | /* --------------------------------------------------- */ 15 | 16 | internal fun Fragment.createLauncher(onCallback: (ActivityResult) -> Unit) = 17 | registerForActivityResult( 18 | ActivityResultContracts.StartActivityForResult(), 19 | onCallback 20 | ) 21 | 22 | internal fun ComponentActivity.createLauncher(onCallback: (ActivityResult) -> Unit) = 23 | registerForActivityResult( 24 | ActivityResultContracts.StartActivityForResult(), 25 | onCallback 26 | ) 27 | 28 | /* --------------------------------------------------- */ 29 | /* > Execution */ 30 | /* --------------------------------------------------- */ 31 | 32 | internal fun RouterProcessor<*>.pushToProcessor( 33 | launcher: ActivityResultLauncher, 34 | route: BaseRoute<*>, 35 | param: Any? = null, 36 | ) { 37 | push(ActivityResultLauncherProcessor, ActivityResultLauncherProcessor.Param( 38 | launcher = launcher, 39 | originalParam = param, 40 | originalRoute = route 41 | )) 42 | } 43 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/Scenario.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario 2 | 3 | import nolambda.linkrouter.android.BaseRoute 4 | import nolambda.linkrouter.android.extra.caller.scenario.processor.ScenarioResultProcessor 5 | 6 | abstract class Scenario

{ 7 | abstract val route: BaseRoute

8 | abstract val processor: ScenarioResultProcessor 9 | } 10 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/ScenarioExt.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario 2 | 3 | import android.content.Intent 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.result.ActivityResult 6 | import androidx.activity.result.ActivityResultLauncher 7 | import androidx.fragment.app.Fragment 8 | import nolambda.linkrouter.android.Route 9 | import nolambda.linkrouter.android.RouteWithParam 10 | import nolambda.linkrouter.android.extra.caller.createLauncher 11 | import nolambda.linkrouter.android.extra.caller.scenario.launcher.ParameterizedScenarioLauncher 12 | import nolambda.linkrouter.android.extra.caller.scenario.launcher.ScenarioLauncher 13 | import nolambda.linkrouter.android.extra.caller.scenario.processor.OnResult 14 | import nolambda.linkrouter.android.extra.caller.scenario.processor.RetainedScenarioResultProcessor 15 | 16 | fun

ComponentActivity.registerScenarioForResult( 17 | scenario: Scenario, 18 | onCallback: (R) -> Unit 19 | ): ParameterizedScenarioLauncher

{ 20 | val host = ActivityHost(this) 21 | val internalLauncher = createInternalLauncher(host, scenario, onCallback) 22 | return ParameterizedScenarioLauncher(internalLauncher, scenario.route as RouteWithParam

) 23 | } 24 | 25 | fun ComponentActivity.registerScenarioForResult( 26 | scenario: Scenario, 27 | onCallback: (R) -> Unit 28 | ): ScenarioLauncher { 29 | val host = ActivityHost(this) 30 | val internalLauncher = createInternalLauncher(host, scenario, onCallback) 31 | return ScenarioLauncher(internalLauncher, scenario.route as Route) 32 | } 33 | 34 | fun

Fragment.registerScenarioForResult( 35 | scenario: Scenario, 36 | onCallback: (R) -> Unit 37 | ): ParameterizedScenarioLauncher

{ 38 | val host = FragmentHost(this) 39 | val internalLauncher = createInternalLauncher(host, scenario, onCallback) 40 | return ParameterizedScenarioLauncher(internalLauncher, scenario.route as RouteWithParam

) 41 | } 42 | 43 | fun Fragment.registerScenarioForResult( 44 | scenario: Scenario, 45 | onCallback: (R) -> Unit 46 | ): ScenarioLauncher { 47 | val host = FragmentHost(this) 48 | val internalLauncher = createInternalLauncher(host, scenario, onCallback) 49 | return ScenarioLauncher(internalLauncher, scenario.route as Route) 50 | } 51 | 52 | private fun createInternalLauncher( 53 | host: ScenarioHost, 54 | scenario: Scenario<*, R>, 55 | onCallback: (R) -> Unit 56 | ): ActivityResultLauncher { 57 | val processor = scenario.processor 58 | 59 | val continuation = { activityResult: ActivityResult -> 60 | processor.process(activityResult, null, object : OnResult { 61 | override fun continueWith(result: R) { 62 | onCallback(result) 63 | 64 | if (processor is RetainedScenarioResultProcessor) { 65 | processor.onClear() 66 | } 67 | } 68 | }) 69 | } 70 | if (processor is RetainedScenarioResultProcessor) { 71 | processor.onRegister(host, continuation) 72 | } 73 | return host.createLauncher(continuation) 74 | } 75 | 76 | private fun ScenarioHost.createLauncher(callback: (ActivityResult) -> Unit) = when (this) { 77 | is ActivityHost -> activity.createLauncher(callback) 78 | is FragmentHost -> fragment.createLauncher(callback) 79 | } 80 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/ScenarioHost.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.fragment.app.Fragment 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.lifecycle.ViewModelStoreOwner 7 | 8 | sealed class ScenarioHost 9 | class ActivityHost(val activity: ComponentActivity) : ScenarioHost() 10 | class FragmentHost(val fragment: Fragment) : ScenarioHost() 11 | 12 | val ScenarioHost.viewModelStore: ViewModelStoreOwner 13 | get() = when (this) { 14 | is ActivityHost -> activity 15 | is FragmentHost -> fragment 16 | } 17 | 18 | val ScenarioHost.lifecycle: Lifecycle 19 | get() = when (this) { 20 | is ActivityHost -> activity.lifecycle 21 | is FragmentHost -> fragment.lifecycle 22 | } 23 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/launcher/ParameterizedScenarioLauncher.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario.launcher 2 | 3 | import android.content.Intent 4 | import androidx.activity.result.ActivityResultLauncher 5 | import nolambda.linkrouter.android.RouteWithParam 6 | import nolambda.linkrouter.android.RouterProcessor 7 | import nolambda.linkrouter.android.extra.caller.pushToProcessor 8 | 9 | class ParameterizedScenarioLauncher

( 10 | private val launcher: ActivityResultLauncher, 11 | private val route: RouteWithParam

12 | ) { 13 | fun launch(router: RouterProcessor<*>, p: P) { 14 | router.pushToProcessor(launcher, route, p) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/launcher/ScenarioLauncher.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario.launcher 2 | 3 | import android.content.Intent 4 | import androidx.activity.result.ActivityResultLauncher 5 | import nolambda.linkrouter.android.Route 6 | import nolambda.linkrouter.android.RouterProcessor 7 | import nolambda.linkrouter.android.extra.caller.pushToProcessor 8 | 9 | class ScenarioLauncher( 10 | private val launcher: ActivityResultLauncher, 11 | private val route: Route 12 | ) { 13 | fun launch(router: RouterProcessor<*>) { 14 | router.pushToProcessor(launcher, route) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/processor/ComposedResultProcessor.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario.processor 2 | 3 | import androidx.activity.result.ActivityResult 4 | 5 | class ComposedResultProcessor( 6 | private val processors: List> 7 | ) : ScenarioResultProcessor { 8 | 9 | init { 10 | check(processors.isNotEmpty()) { 11 | "Processors cannot be empty!" 12 | } 13 | } 14 | 15 | override fun process(result: ActivityResult, lastResult: R?, onResult: OnResult) { 16 | callProcessor(0, result, lastResult, onResult) 17 | } 18 | 19 | private fun callProcessor( 20 | index: Int, 21 | activityResult: ActivityResult, 22 | lastResult: R?, 23 | onResult: OnResult 24 | ) { 25 | val currentProcessor = processors[index] 26 | currentProcessor.process(activityResult, lastResult, object : OnResult { 27 | override fun continueWith(result: R) { 28 | val nextIndex = index + 1 29 | val nextProcessor = processors.getOrNull(nextIndex) 30 | if (nextProcessor == null) { 31 | onResult.continueWith(result) 32 | return 33 | } 34 | callProcessor(nextIndex, activityResult, result, onResult) 35 | } 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/processor/OnResult.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario.processor 2 | 3 | interface OnResult { 4 | fun continueWith(result: R) 5 | } -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/processor/ResultProcessorIndexTracker.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario.processor 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | 6 | internal class ResultProcessorIndexTracker(saveState: SavedStateHandle) : ViewModel() { 7 | companion object { 8 | const val FIRST_INDEX = 0 9 | } 10 | 11 | var index: Int = saveState.get("index") ?: FIRST_INDEX 12 | 13 | fun reset() { 14 | index = FIRST_INDEX 15 | } 16 | } -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/processor/RetainedComposedResultProcessor.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario.processor 2 | 3 | import androidx.activity.result.ActivityResult 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleEventObserver 6 | import androidx.lifecycle.LifecycleOwner 7 | import androidx.lifecycle.ViewModelProvider 8 | import nolambda.linkrouter.android.extra.caller.scenario.ScenarioHost 9 | import nolambda.linkrouter.android.extra.caller.scenario.lifecycle 10 | import nolambda.linkrouter.android.extra.caller.scenario.viewModelStore 11 | 12 | class RetainedComposedResultProcessor( 13 | private val processors: List> 14 | ) : RetainedScenarioResultProcessor { 15 | 16 | private lateinit var tracker: ResultProcessorIndexTracker 17 | 18 | init { 19 | check(processors.isNotEmpty()) { 20 | "Composed processors cannot be empty!" 21 | } 22 | } 23 | 24 | override fun process(result: ActivityResult, lastResult: R?, onResult: OnResult) { 25 | val firstIndex = tracker.index 26 | callProcessors(firstIndex, result, lastResult, onResult) 27 | } 28 | 29 | private fun callProcessors( 30 | index: Int, 31 | activityResult: ActivityResult, 32 | lastResult: R?, 33 | onResult: OnResult 34 | ) { 35 | val currentProcessor = processors[index] 36 | currentProcessor.process(activityResult, lastResult, object : OnResult { 37 | override fun continueWith(result: R) { 38 | val nextIndex = index + 1 39 | val nextProcessor = processors.getOrNull(nextIndex) 40 | if (nextProcessor == null) { 41 | // call last callback 42 | onResult.continueWith(result) 43 | // reset the retain index 44 | tracker.reset() 45 | return 46 | } 47 | 48 | callProcessors(nextIndex, activityResult, result, onResult) 49 | tracker.index = index 50 | } 51 | }) 52 | } 53 | 54 | override fun onRegister(host: ScenarioHost, continuation: (ActivityResult) -> Unit) { 55 | host.lifecycle.addObserver(object : LifecycleEventObserver { 56 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 57 | if (event == Lifecycle.Event.ON_CREATE) { 58 | tracker = ViewModelProvider(host.viewModelStore).get(ResultProcessorIndexTracker::class.java) 59 | } 60 | } 61 | }) 62 | 63 | processors.forEach { 64 | it.onRegister(host, continuation) 65 | } 66 | } 67 | 68 | override fun onClear() { 69 | processors.forEach { it.onClear() } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/processor/RetainedScenarioResultProcessor.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario.processor 2 | 3 | import androidx.activity.result.ActivityResult 4 | import nolambda.linkrouter.android.extra.caller.scenario.ScenarioHost 5 | 6 | interface RetainedScenarioResultProcessor : ScenarioResultProcessor { 7 | fun onRegister(host: ScenarioHost, continuation: (ActivityResult) -> Unit) 8 | fun onClear() 9 | } 10 | -------------------------------------------------------------------------------- /extras/android-activityresult/src/main/java/nolambda/linkrouter/android/extra/caller/scenario/processor/ScenarioResultProcessor.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller.scenario.processor 2 | 3 | import androidx.activity.result.ActivityResult 4 | 5 | interface ScenarioResultProcessor { 6 | fun process(result: ActivityResult, lastResult: R?, onResult: OnResult) 7 | } -------------------------------------------------------------------------------- /extras/android-caller/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /extras/android-caller/build.gradle: -------------------------------------------------------------------------------- 1 | apply from: "${buildSystemDir}/base.gradle" 2 | apply plugin: 'maven-publish' 3 | 4 | dependencies { 5 | implementation project(':android') 6 | implementation deps.appCompat 7 | 8 | testImplementation testDeps 9 | testImplementation project(':android-test') 10 | } 11 | 12 | afterEvaluate { 13 | publishing { 14 | publications { 15 | release(MavenPublication) { 16 | from components.release 17 | 18 | groupId = appGroupId 19 | artifactId = project.name 20 | version = appVersion 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /extras/android-caller/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esafirm/universal-router/9fa8b8a1e0f7d728e4b229105b1a7ceeb0d700b0/extras/android-caller/consumer-rules.pro -------------------------------------------------------------------------------- /extras/android-caller/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /extras/android-caller/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /extras/android-caller/src/main/java/nolambda/linkrouter/android/extra/caller/CallerAppRouter.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller 2 | 3 | import nolambda.linkrouter.android.AbstractAppRouter 4 | import nolambda.linkrouter.android.Route 5 | import nolambda.linkrouter.android.RouteResult 6 | import nolambda.linkrouter.android.RouteWithParam 7 | import nolambda.linkrouter.android.RouterProcessor 8 | 9 | class CallerAppRouter( 10 | private val appRouter: AbstractAppRouter, 11 | private val caller: Any 12 | ) : RouterProcessor by appRouter { 13 | override fun push(route: Route): RouteResult { 14 | return appRouter.push(CallerProcessor, CallerProcessor.Param( 15 | caller = caller, 16 | originalRoute = route 17 | )) 18 | } 19 | 20 | override fun

push(route: RouteWithParam

, param: P): RouteResult { 21 | return appRouter.push(CallerProcessor, CallerProcessor.Param( 22 | caller, 23 | param, 24 | route 25 | )) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /extras/android-caller/src/main/java/nolambda/linkrouter/android/extra/caller/CallerProcessor.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller 2 | 3 | import nolambda.linkrouter.android.BaseRoute 4 | import nolambda.linkrouter.android.RouteWithParam 5 | 6 | object CallerProcessor : RouteWithParam() { 7 | data class Param( 8 | val caller: Any, 9 | val originalParam: Any? = null, 10 | val originalRoute: BaseRoute<*> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /extras/android-caller/src/main/java/nolambda/linkrouter/android/extra/caller/CallerProviderMiddleware.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller 2 | 3 | import nolambda.linkrouter.android.BaseRoute 4 | import nolambda.linkrouter.android.RouteParam 5 | import nolambda.linkrouter.android.middlewares.MiddleWareResult 6 | import nolambda.linkrouter.android.middlewares.Middleware 7 | 8 | class CallerProviderMiddleware( 9 | private val onCreateExtra: (Extra?, Any?) -> Extra 10 | ) : Middleware { 11 | override fun onRouting(route: BaseRoute<*>, routeParam: RouteParam<*, Extra>): MiddleWareResult { 12 | val navParam = routeParam.param 13 | if (route is CallerProcessor && navParam is CallerProcessor.Param) { 14 | return MiddleWareResult(navParam.originalRoute, routeParam.copyWithParam( 15 | passedParam = navParam.originalParam, 16 | passedExtra = onCreateExtra(routeParam.extra, navParam.caller) 17 | )) 18 | } 19 | 20 | return MiddleWareResult(route, routeParam) 21 | } 22 | } -------------------------------------------------------------------------------- /extras/android-caller/src/test/java/nolambda/linkrouter/android/extra/caller/CallerAppRouterSpec.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.android.extra.caller 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.shouldBe 5 | import nolambda.linkrouter.android.AbstractAppRouter 6 | import nolambda.linkrouter.android.test.TestRoute 7 | 8 | class CallerAppRouterSpec : BehaviorSpec({ 9 | Given("Router with caller middleware") { 10 | val testRouter = object : AbstractAppRouter(CallerProviderMiddleware { _, caller -> 11 | caller as String 12 | }) {} 13 | 14 | When("Call with string caller") { 15 | val caller = "Caller" 16 | val router = CallerAppRouter(testRouter, caller) 17 | val testRoute = TestRoute() 18 | 19 | var extra: String? = null 20 | testRouter.register(testRoute) { 21 | extra = it.extra 22 | } 23 | router.push(testRoute) 24 | 25 | Then("The extra should be the caller") { 26 | extra shouldBe caller 27 | } 28 | } 29 | } 30 | }) -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx1536m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esafirm/universal-router/9fa8b8a1e0f7d728e4b229105b1a7ceeb0d700b0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jan 28 12:35:11 WIB 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | before_install: 4 | - ./setup.sh 5 | -------------------------------------------------------------------------------- /processor/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /processor/.kotlintest/spec_failures: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esafirm/universal-router/9fa8b8a1e0f7d728e4b229105b1a7ceeb0d700b0/processor/.kotlintest/spec_failures -------------------------------------------------------------------------------- /processor/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | test { 6 | useJUnitPlatform() 7 | } 8 | 9 | dependencies { 10 | implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 11 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 12 | implementation project(':annotations') 13 | 14 | implementation 'com.google.auto.service:auto-service:1.0.1' 15 | kapt 'com.google.auto.service:auto-service:1.0-rc6' 16 | 17 | testImplementation testDeps 18 | } 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | compileKotlin { 25 | kotlinOptions { 26 | jvmTarget = "1.8" 27 | } 28 | } 29 | compileTestKotlin { 30 | kotlinOptions { 31 | jvmTarget = "1.8" 32 | } 33 | } -------------------------------------------------------------------------------- /processor/src/main/java/nolambda/linkrouter/processor/ProcessorExt.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.processor 2 | 3 | import javax.annotation.processing.ProcessingEnvironment 4 | import javax.tools.Diagnostic 5 | 6 | class Logger(private val env: ProcessingEnvironment) { 7 | fun error(message: String) { 8 | print(Diagnostic.Kind.ERROR, message) 9 | } 10 | 11 | fun note(message: String) { 12 | print(Diagnostic.Kind.NOTE, message) 13 | } 14 | 15 | private fun print(kind: Diagnostic.Kind, message: String) { 16 | env.messager.printMessage(kind, message) 17 | } 18 | } -------------------------------------------------------------------------------- /processor/src/main/java/nolambda/linkrouter/processor/RouteInitGenerator.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.processor 2 | 3 | import java.io.File 4 | 5 | class RouteInitGenerator( 6 | private val destDirectory: String, 7 | private val routeInits: List 8 | ) { 9 | 10 | companion object { 11 | private const val FILE_SUFFIX = "RouteInit.kt" 12 | private const val PACKAGE = "nolambda/init/route" 13 | } 14 | 15 | fun generate() { 16 | val packageName = PACKAGE.replace("/", ".") 17 | val realDest = File("$destDirectory/$PACKAGE") 18 | 19 | if (!realDest.exists()) realDest.mkdirs() 20 | 21 | routeInits.forEach { 22 | val routeClass = it.routeClass 23 | val identifier = routeClass.substring(routeClass.lastIndexOf(".") + 1) 24 | val name = "${identifier}$FILE_SUFFIX" 25 | val file = File(realDest, name) 26 | file.writeText(createClass(packageName, it, identifier, routeClass)) 27 | } 28 | } 29 | 30 | private fun createClass( 31 | packageName: String, 32 | node: RouteInitNode, 33 | routeIdentifier: String, 34 | routeClass: String 35 | ): String { 36 | return """ 37 | package $packageName 38 | 39 | import android.content.Context 40 | import ${node.packageName}.${node.className} 41 | 42 | class ${routeIdentifier}RouteInit(private val appContext: Context) { 43 | init { 44 | ${node.className}::class.java.newInstance().onInit(appContext) 45 | } 46 | } 47 | """.trimIndent() 48 | } 49 | 50 | } 51 | 52 | -------------------------------------------------------------------------------- /processor/src/main/java/nolambda/linkrouter/processor/RouteInitNode.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.processor 2 | 3 | data class RouteInitNode( 4 | val className: String, 5 | val packageName: String, 6 | val routeClass: String 7 | ) -------------------------------------------------------------------------------- /processor/src/main/java/nolambda/linkrouter/processor/RouteInitProcessor.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.processor 2 | 3 | import com.google.auto.service.AutoService 4 | import nolambda.linkrouter.annotations.Navigate 5 | import javax.annotation.processing.AbstractProcessor 6 | import javax.annotation.processing.Processor 7 | import javax.annotation.processing.RoundEnvironment 8 | import javax.annotation.processing.SupportedSourceVersion 9 | import javax.lang.model.SourceVersion 10 | import javax.lang.model.element.ElementKind 11 | import javax.lang.model.element.TypeElement 12 | import javax.lang.model.type.MirroredTypeException 13 | 14 | @SupportedSourceVersion(SourceVersion.RELEASE_8) 15 | @AutoService(Processor::class) 16 | class RouteInitProcessor : AbstractProcessor() { 17 | 18 | companion object { 19 | private const val OPTION_KAPT_KOTLIN_GENERATED = "kapt.kotlin.generated" 20 | } 21 | 22 | private val logger by lazy { Logger(processingEnv) } 23 | 24 | override fun getSupportedAnnotationTypes(): MutableSet { 25 | return mutableSetOf(Navigate::class.java.name) 26 | } 27 | 28 | override fun process( 29 | p0: MutableSet?, 30 | env: RoundEnvironment 31 | ): Boolean { 32 | val routeInits = mutableListOf() 33 | 34 | env.getElementsAnnotatedWith(Navigate::class.java).forEach { el -> 35 | if (el.kind != ElementKind.CLASS) { 36 | logger.error("Annotation can only be applied to content provider or method") 37 | return false 38 | } 39 | 40 | logger.note("Note: ${el.enclosedElements.joinToString { it.simpleName }}") 41 | 42 | val annotation = el.getAnnotation(Navigate::class.java) 43 | val pack = processingEnv.elementUtils.getPackageOf(el).toString() 44 | routeInits.add( 45 | RouteInitNode(el.simpleName.toString(), pack, getRouteClass(annotation)) 46 | ) 47 | } 48 | 49 | if (routeInits.isEmpty().not()) { 50 | generateFiles(routeInits) 51 | } 52 | 53 | return true 54 | } 55 | 56 | private fun getRouteClass(navigate: Navigate): String { 57 | return try { 58 | navigate.route.simpleName!! 59 | } catch (mte: MirroredTypeException) { 60 | mte.typeMirror.toString() 61 | } 62 | } 63 | 64 | private fun generateFiles(routeInits: List) { 65 | val dest = processingEnv.options[OPTION_KAPT_KOTLIN_GENERATED] 66 | ?: throw IllegalStateException("Kapt option not exist") 67 | RouteInitGenerator(dest, routeInits).generate() 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /processor/src/test/java/nolambda/linkrouter/processor/RouteInitGeneratorSpec.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.processor 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | import java.io.File 6 | 7 | class RouteInitGeneratorSpec : StringSpec({ 8 | "It should be generated" { 9 | val nodes = listOf( 10 | RouteInitNode( 11 | "ProductInit", 12 | "com.something", 13 | "String" 14 | ) 15 | ) 16 | 17 | @Suppress("BlockingMethodInNonBlockingContext") 18 | val temp = File.createTempFile("RouteInit", ".kt").parentFile 19 | 20 | val generator = RouteInitGenerator(temp.absolutePath, nodes) 21 | generator.generate() 22 | 23 | val dest = File("${temp.absoluteFile}/nolambda/init/route/StringRouteInit.kt") 24 | dest.exists() shouldBe true 25 | 26 | val text = dest.readText() 27 | 28 | text.contains("class StringRouteInit") shouldBe true 29 | text.contains("ProductInit::class.java.newInstance().onInit(appContext)") shouldBe true 30 | text.contains("import android.content.Context") shouldBe true 31 | text.contains("import com.something.ProductInit") shouldBe true 32 | 33 | dest.delete() 34 | } 35 | }) -------------------------------------------------------------------------------- /processor/src/test/resources/manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /samples/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /samples/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | signingConfigs { 7 | release { 8 | keyAlias 'test' 9 | keyPassword 'test123' 10 | storePassword 'test123' 11 | storeFile file('universalrouter.jks') 12 | } 13 | } 14 | 15 | compileSdkVersion versions.compileSdk 16 | defaultConfig { 17 | applicationId "nolambda.linkrouter.samples" 18 | minSdkVersion 21 19 | targetSdkVersion versions.compileSdk 20 | versionCode 1 21 | versionName "1.0" 22 | 23 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 24 | } 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 29 | signingConfig signingConfigs.release 30 | } 31 | } 32 | kotlinOptions { 33 | jvmTarget = "1.8" 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation project(':samples:approuter') 39 | implementation project(':samples:cart') 40 | implementation project(':samples:product') 41 | 42 | implementation('androidx.appcompat:appcompat:1.3.1') 43 | 44 | /* UI Test */ 45 | final espressoVersion = '3.4.0' 46 | final runnerVersion = '1.3.0' 47 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 48 | androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" 49 | androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" 50 | androidTestImplementation "androidx.test:runner:$runnerVersion" 51 | androidTestImplementation "androidx.test:rules:$runnerVersion" 52 | androidTestImplementation 'io.github.kakaocup:kakao:3.0.2' 53 | } 54 | -------------------------------------------------------------------------------- /samples/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /samples/app/src/androidTest/java/nolambda/linkrouter/examples/PerformanceTest.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples 2 | 3 | import androidx.test.ext.junit.rules.ActivityScenarioRule 4 | import io.github.kakaocup.kakao.screen.Screen 5 | import org.junit.Ignore 6 | import org.junit.Rule 7 | import org.junit.Test 8 | 9 | class PerformanceTest { 10 | 11 | @Rule 12 | @JvmField 13 | val rule = ActivityScenarioRule(PerformanceTestActivity::class.java) 14 | 15 | @Test 16 | fun standardRouting() { 17 | Screen.onScreen { 18 | btnTest.click() 19 | } 20 | } 21 | 22 | @Test 23 | fun noUrlRouting() { 24 | Screen.onScreen { 25 | switchIsNoUrl.click() 26 | btnTest.click() 27 | } 28 | } 29 | 30 | @Test 31 | fun lazyRouting() { 32 | Screen.onScreen { 33 | switchIsLazy.click() 34 | btnTest.click() 35 | } 36 | } 37 | 38 | @Ignore 39 | @Test 40 | fun lazyNoUrlRouting() { 41 | Screen.onScreen { 42 | switchIsLazy.click() 43 | switchIsNoUrl.click() 44 | btnTest.click() 45 | } 46 | } 47 | 48 | @Test 49 | fun keyUriRouter() { 50 | Screen.onScreen { 51 | switchKeyUri.click() 52 | btnTest.click() 53 | } 54 | } 55 | 56 | @Test 57 | fun lazyKeyUriRouter() { 58 | Screen.onScreen { 59 | switchIsLazy.click() 60 | switchKeyUri.click() 61 | btnTest.click() 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /samples/app/src/androidTest/java/nolambda/linkrouter/examples/PerformanceTestScreen.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples 2 | 3 | import io.github.kakaocup.kakao.screen.Screen 4 | import io.github.kakaocup.kakao.switch.KSwitch 5 | import io.github.kakaocup.kakao.text.KButton 6 | 7 | class PerformanceTestScreen : Screen() { 8 | val btnTest = KButton { withId(R.id.btn_test) } 9 | val switchIsLazy = KSwitch { withId(R.id.switch_lazy) } 10 | val switchIsNoUrl = KSwitch { withId(R.id.switch_no_url) } 11 | val switchKeyUri = KSwitch { withId(R.id.switch_key_uri) } 12 | } -------------------------------------------------------------------------------- /samples/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/FragmentRouter.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples 2 | 3 | import androidx.fragment.app.Fragment 4 | import nolambda.linkrouter.SimpleUriRouter 5 | 6 | class FragmentRouter(private val activity: MainActivity) { 7 | 8 | private val simpleUriRouter = SimpleUriRouter() 9 | 10 | private val fragmentManager by lazy { activity.supportFragmentManager } 11 | 12 | init { 13 | simpleUriRouter.addEntry("sample://fragment/{text}") { _, it -> 14 | SampleFragment.newInstance(it["text"] ?: throw IllegalStateException("Uri not valid")) 15 | } 16 | } 17 | 18 | fun goTo(uri: String) { 19 | val fragment = simpleUriRouter.resolve(uri) 20 | ?: throw IllegalStateException("No path for $uri") 21 | fragmentManager.beginTransaction() 22 | .replace(R.id.container, fragment) 23 | .commitAllowingStateLoss() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | import androidx.appcompat.app.AppCompatActivity 8 | import kotlinx.android.synthetic.main.activity_main_simple.* 9 | import nolambda.linkrouter.examples.notsimple.NotSimpleActivity 10 | import nolambda.linkrouter.examples.picker.PickerExampleFragmentScreen 11 | import nolambda.linkrouter.examples.picker.PickerExampleScreen 12 | 13 | class MainActivity : AppCompatActivity() { 14 | 15 | private val router by lazy { FragmentRouter(this) } 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_main_simple) 20 | 21 | btnNavigate.setOnClickListener { 22 | startActivity(Intent(applicationContext, NotSimpleActivity::class.java)) 23 | } 24 | 25 | /* --------------------------------------------------- */ 26 | /* > Picker */ 27 | /* --------------------------------------------------- */ 28 | 29 | btnNavigatePicker.setOnClickListener { 30 | startActivity(Intent(applicationContext, PickerExampleScreen::class.java)) 31 | } 32 | 33 | btnNavigatePickerFragment.setOnClickListener { 34 | startActivity(Intent(applicationContext, PickerExampleFragmentScreen::class.java)) 35 | } 36 | 37 | /* --------------------------------------------------- */ 38 | /* > Simple */ 39 | /* --------------------------------------------------- */ 40 | 41 | btnNavigateTwo.setOnClickListener { 42 | router.goTo("sample://fragment/first") 43 | 44 | val handler = Handler(Looper.getMainLooper()) 45 | 46 | handler.postDelayed({ 47 | router.goTo("sample://fragment/second") 48 | }, 2000) 49 | 50 | handler.postDelayed({ 51 | router.goTo("sample://fragment/third") 52 | }, 5000) 53 | } 54 | 55 | /* --------------------------------------------------- */ 56 | /* > Performance */ 57 | /* --------------------------------------------------- */ 58 | 59 | btnNavPerformanceTest.setOnClickListener { 60 | startActivity(Intent(applicationContext, PerformanceTestActivity::class.java)) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/PerformanceTestActivity.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import androidx.appcompat.app.AppCompatActivity 6 | import kotlinx.android.synthetic.main.activity_performance_test.* 7 | import nolambda.linkrouter.DeepLinkEntry 8 | import nolambda.linkrouter.android.AbstractAppRouter 9 | import nolambda.linkrouter.android.KeyUriRouterFactory 10 | import nolambda.linkrouter.android.Route 11 | import nolambda.linkrouter.android.SimpleUriRouterFactory 12 | import nolambda.linkrouter.android.registerstrategy.EagerRegisterStrategy 13 | import nolambda.linkrouter.android.registerstrategy.LazyRegisterStrategy 14 | import nolambda.linkrouter.examples.utils.isDebuggable 15 | import nolambda.linkrouter.matcher.DeepLinkEntryMatcher 16 | import kotlin.random.Random 17 | import kotlin.system.measureTimeMillis 18 | 19 | class PerformanceTestActivity : AppCompatActivity() { 20 | 21 | companion object { 22 | private const val ROUTES_SIZE = 20_000 23 | } 24 | 25 | override fun onCreate(savedInstanceState: Bundle?) { 26 | super.onCreate(savedInstanceState) 27 | setContentView(R.layout.activity_performance_test) 28 | 29 | btn_test.setOnClickListener { 30 | txt_result.text = "" 31 | 32 | val isLazy = switch_lazy.isChecked 33 | val isNoUrl = switch_no_url.isChecked 34 | val isKeyUri = switch_key_uri.isChecked 35 | 36 | warmUp() 37 | 38 | doTest( 39 | isNoUrl = isNoUrl, 40 | isLazy = isLazy, 41 | isKeyUri = isKeyUri 42 | ) 43 | } 44 | } 45 | 46 | private fun warmUp() { 47 | log("is debug: ${BuildConfig.DEBUG}") 48 | log("is debuggable: ${isDebuggable()}") 49 | 50 | measureWithPrint( 51 | log = { "warm up took $it ms" }, 52 | block = { 53 | // This have initialization cost 54 | DeepLinkEntry.parse("https://test.com/go") 55 | } 56 | ) 57 | } 58 | 59 | private fun createRoutes(havePath: Boolean): List { 60 | if (havePath) { 61 | return (0 until ROUTES_SIZE).map { 62 | object : Route("https://test.com/${Random.nextInt(5)}/$it/{a}") {} 63 | } 64 | } 65 | return (0 until ROUTES_SIZE).map { 66 | object : Route() {} 67 | } 68 | } 69 | 70 | private fun getTag( 71 | isNoUrl: Boolean, 72 | isLazy: Boolean, 73 | isKeyUri: Boolean 74 | ): String { 75 | return when { 76 | isKeyUri && isLazy -> "[KL]" 77 | isNoUrl && isLazy -> "[NL]" 78 | isNoUrl -> "[N]" 79 | isLazy -> "[L]" 80 | isKeyUri -> "[K]" 81 | else -> "[E]" // eager 82 | } 83 | } 84 | 85 | private fun doTest( 86 | isNoUrl: Boolean, 87 | isLazy: Boolean, 88 | isKeyUri: Boolean 89 | ) { 90 | val logger = ::log 91 | val routes = createRoutes(havePath = isNoUrl.not()) 92 | val testRouter = object : AbstractAppRouter( 93 | registerStrategy = when (isLazy) { 94 | true -> LazyRegisterStrategy() 95 | false -> EagerRegisterStrategy() 96 | }, 97 | uriRouterFactory = when (isKeyUri) { 98 | true -> KeyUriRouterFactory(logger) { 99 | val uri = java.net.URL(it) 100 | val paths = uri.path.split("/") 101 | "${paths[1]}${paths[2]}" 102 | } 103 | false -> SimpleUriRouterFactory(logger) 104 | } 105 | ) {} 106 | val size = routes.size 107 | 108 | val tag = getTag(isNoUrl, isLazy, isKeyUri) 109 | val t1 = measureWithPrint( 110 | log = { time -> "$tag registers took $time ms for $size entries" }, 111 | block = { 112 | routes.forEach { route -> 113 | testRouter.addEntry(route) 114 | } 115 | } 116 | ) 117 | 118 | // Get test route and assign the variable 119 | val route = routes.random().routePaths.first() 120 | .replace("{a}", Random.nextInt().toString()) 121 | 122 | val t2 = measureWithPrint( 123 | log = { time -> "$tag goTo took $time ms to resolve from $size entries" }, 124 | block = { testRouter.goTo(route) } 125 | ) 126 | 127 | val t3 = measureWithPrint( 128 | log = { time -> "$tag second goTo took $time ms to resolve from $size entries" }, 129 | block = { testRouter.goTo(route) } 130 | ) 131 | 132 | log("Total ${t1 + t2 + t3} ms") 133 | } 134 | 135 | private inline fun measureWithPrint( 136 | noinline log: (time: Long) -> String, 137 | block: () -> Unit 138 | ): Long { 139 | val time = measureTimeMillis(block) 140 | val logString = log(time) 141 | log(logString) 142 | return time 143 | } 144 | 145 | private fun AbstractAppRouter.addEntry(route: Route) { 146 | register(route) { 147 | log("Triggered: ${it.info.uri}") 148 | } 149 | } 150 | 151 | private fun log(text: String) { 152 | txt_result.append(text) 153 | txt_result.append("\n") 154 | Log.d("Perf", text) 155 | println(text) 156 | } 157 | } -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/SampleApp.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import nolambda.linkrouter.android.RouterPlugin 6 | 7 | class SampleApp : Application() { 8 | override fun onCreate() { 9 | super.onCreate() 10 | 11 | RouterPlugin.logger = { log: String -> Log.d("Router", log) } 12 | } 13 | } -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/SampleFragment.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | 9 | class SampleFragment : androidx.fragment.app.Fragment() { 10 | 11 | companion object { 12 | @JvmStatic 13 | fun newInstance(text: String): SampleFragment = SampleFragment().apply { 14 | arguments = Bundle().apply { 15 | putString("text", text) 16 | } 17 | } 18 | } 19 | 20 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 21 | return inflater.inflate(R.layout.fragment_sample, container, false).apply { 22 | val text = findViewById(R.id.text) 23 | text.text = arguments?.getString("text") 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/notsimple/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples.notsimple 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import kotlinx.android.synthetic.main.fragment_sample_home.* 7 | import nolambda.linkrouter.approuter.AppRouter 8 | import nolambda.linkrouter.approuter.AppRoutes 9 | import nolambda.linkrouter.examples.R 10 | 11 | class HomeScreen : Fragment(R.layout.fragment_sample_home) { 12 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 13 | btnGoToProductOne.setOnClickListener { 14 | AppRouter.push(AppRoutes.Product, AppRoutes.Product.ProductParam("123")) 15 | } 16 | btnGoToProductTwo.setOnClickListener { 17 | AppRouter.goTo("https://m.bukatoko.com/product/123") 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/notsimple/NotSimpleActivity.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples.notsimple 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.fragment.app.Fragment 6 | import nolambda.linkrouter.android.RouterPlugin 7 | import nolambda.linkrouter.android.addRouterProcessor 8 | import nolambda.linkrouter.android.autoregister.AutoRegister 9 | import nolambda.linkrouter.approuter.AppRouter 10 | import nolambda.linkrouter.approuter.AppRoutes 11 | import nolambda.linkrouter.approuter.register 12 | import nolambda.linkrouter.examples.R 13 | 14 | class NotSimpleActivity : AppCompatActivity() { 15 | 16 | @AutoRegister 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_main) 20 | 21 | RouterPlugin.run { 22 | appContext = this@NotSimpleActivity.applicationContext 23 | isUseAnnotationProcessor = true 24 | } 25 | 26 | AppRoutes.Home.register { 27 | HomeScreen() 28 | } 29 | 30 | addRouterProcessor(AppRouter) { fragment, _ -> 31 | supportFragmentManager.beginTransaction() 32 | .replace(R.id.container, fragment) 33 | .commit() 34 | } 35 | 36 | AppRouter.push(AppRoutes.Home) 37 | } 38 | } -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/picker/PickerExampleFragmentScreen.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples.picker 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.fragment.app.Fragment 8 | import kotlinx.android.synthetic.main.activity_picker_example.* 9 | import nolambda.linkrouter.android.extra.caller.registerRouteForResult 10 | import nolambda.linkrouter.android.extra.caller.scenario.registerScenarioForResult 11 | import nolambda.linkrouter.approuter.AppRouter 12 | import nolambda.linkrouter.approuter.register 13 | import nolambda.linkrouter.examples.R 14 | 15 | class PickerExampleFragmentScreen : AppCompatActivity(R.layout.activity_main) { 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | 19 | if (savedInstanceState == null) { 20 | supportFragmentManager.beginTransaction() 21 | .add(R.id.container, PickerFragment()) 22 | .commit() 23 | } 24 | } 25 | } 26 | 27 | class PickerFragment : Fragment(R.layout.activity_picker_example) { 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | 31 | ResultPickerRoute.register { 32 | val intent = Intent(requireContext(), ResultPickerScreen::class.java) 33 | val launcher = it.extra?.launcher 34 | launcher?.launch(intent) 35 | } 36 | 37 | btn_picker.setOnClickListener { 38 | showPicker(AppRouter) 39 | } 40 | 41 | btn_picker_scenario.setOnClickListener { 42 | showPickerScenario.launch(AppRouter) 43 | } 44 | 45 | btn_picker_scenario_simple.setOnClickListener { 46 | simpleScenarioLauncher.launch(AppRouter) 47 | } 48 | } 49 | 50 | private val showPicker = registerRouteForResult(ResultPickerRoute) { 51 | txt_result.text = "Result data: ${it.data?.getStringExtra("result")}" 52 | } 53 | 54 | private val showPickerScenario = registerScenarioForResult(ResultPickerScenario()) { 55 | txt_result.text = it 56 | } 57 | 58 | private val simpleScenarioLauncher = registerScenarioForResult(SimpleResultPickerScenario()) { 59 | txt_result.text = it 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/picker/PickerExampleScreen.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples.picker 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import kotlinx.android.synthetic.main.activity_picker_example.* 7 | import nolambda.linkrouter.android.extra.caller.registerRouteForResult 8 | import nolambda.linkrouter.android.extra.caller.scenario.registerScenarioForResult 9 | import nolambda.linkrouter.approuter.AppRouter 10 | import nolambda.linkrouter.approuter.register 11 | import nolambda.linkrouter.examples.R 12 | 13 | class PickerExampleScreen : AppCompatActivity(R.layout.activity_picker_example) { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | ResultPickerRoute.register { 18 | val launcher = it.extra?.launcher 19 | launcher?.launch(Intent(this, ResultPickerScreen::class.java)) 20 | } 21 | 22 | btn_picker.setOnClickListener { 23 | showPicker(AppRouter) 24 | } 25 | 26 | btn_picker_scenario.setOnClickListener { 27 | showPickerScenario.launch(AppRouter) 28 | } 29 | 30 | btn_picker_scenario_simple.setOnClickListener { 31 | simpleScenarioLauncher.launch(AppRouter) 32 | } 33 | } 34 | 35 | private val showPicker = registerRouteForResult(ResultPickerRoute) { 36 | txt_result.text = "Result data: ${it.data?.getStringExtra("result")}" 37 | } 38 | 39 | private val showPickerScenario = registerScenarioForResult(ResultPickerScenario()) { 40 | txt_result.text = it 41 | } 42 | 43 | private val simpleScenarioLauncher = registerScenarioForResult(SimpleResultPickerScenario()) { 44 | txt_result.text = it 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/picker/ResultPickerRoute.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples.picker 2 | 3 | import android.app.Activity 4 | import androidx.activity.result.ActivityResult 5 | import nolambda.linkrouter.android.Route 6 | import nolambda.linkrouter.android.extra.caller.RouteResultLauncher 7 | import nolambda.linkrouter.android.extra.caller.registerRouteForResult 8 | import nolambda.linkrouter.android.extra.caller.scenario.ActivityHost 9 | import nolambda.linkrouter.android.extra.caller.scenario.FragmentHost 10 | import nolambda.linkrouter.android.extra.caller.scenario.Scenario 11 | import nolambda.linkrouter.android.extra.caller.scenario.ScenarioHost 12 | import nolambda.linkrouter.android.extra.caller.scenario.processor.ComposedResultProcessor 13 | import nolambda.linkrouter.android.extra.caller.scenario.processor.OnResult 14 | import nolambda.linkrouter.android.extra.caller.scenario.processor.RetainedComposedResultProcessor 15 | import nolambda.linkrouter.android.extra.caller.scenario.processor.RetainedScenarioResultProcessor 16 | import nolambda.linkrouter.android.extra.caller.scenario.processor.ScenarioResultProcessor 17 | import nolambda.linkrouter.approuter.AppRouter 18 | 19 | object ResultPickerRoute : Route() 20 | 21 | class PickerResultProcessor : ScenarioResultProcessor { 22 | override fun process(result: ActivityResult, lastResult: String?, onResult: OnResult) { 23 | val text = result.data?.getStringExtra("result") ?: "empty" 24 | onResult.continueWith(text) 25 | } 26 | } 27 | 28 | class ResultPickerScenario : Scenario() { 29 | override val route = ResultPickerRoute 30 | override val processor = RetainedComposedResultProcessor( 31 | processors = listOf(PickAgainResultProcessor(), PickAgainResultProcessor()) 32 | ) 33 | } 34 | 35 | class SimpleResultPickerScenario : Scenario() { 36 | override val route = ResultPickerRoute 37 | override val processor = ComposedResultProcessor( 38 | processors = listOf(SimpleResultProcessor("1"), SimpleResultProcessor("2")) 39 | ) 40 | } 41 | 42 | /* --------------------------------------------------- */ 43 | /* > Test */ 44 | /* --------------------------------------------------- */ 45 | 46 | class SimpleResultProcessor(private val data: String) : ScenarioResultProcessor { 47 | override fun process(result: ActivityResult, lastResult: String?, onResult: OnResult) { 48 | if (lastResult == null) { 49 | onResult.continueWith(data) 50 | } else { 51 | onResult.continueWith("${lastResult}${data}") 52 | } 53 | } 54 | } 55 | 56 | abstract class CallbackScenarioResultProcessor : RetainedScenarioResultProcessor { 57 | 58 | private var processorResult: R? = null 59 | private lateinit var theContinuation: (ActivityResult) -> Unit 60 | 61 | override fun onRegister(host: ScenarioHost, continuation: (ActivityResult) -> Unit) { 62 | theContinuation = continuation 63 | onPrepareCaller(host, object : OnResult { 64 | override fun continueWith(result: R) { 65 | processorResult = result 66 | } 67 | }) 68 | } 69 | 70 | override fun process(result: ActivityResult, lastResult: R?, onResult: OnResult) { 71 | val currentResult = processorResult 72 | if (currentResult != null) { 73 | onResult.continueWith(currentResult) 74 | } else { 75 | onHandleProcess(result, lastResult, onResult) 76 | } 77 | } 78 | 79 | abstract fun onPrepareCaller(host: ScenarioHost, onResult: OnResult) 80 | abstract fun onHandleProcess(result: ActivityResult, lastResult: R?, onResult: OnResult) 81 | 82 | override fun onClear() { 83 | processorResult = null 84 | } 85 | } 86 | 87 | class PickAgainResultProcessor : CallbackScenarioResultProcessor() { 88 | 89 | private lateinit var caller: RouteResultLauncher 90 | 91 | override fun onPrepareCaller(host: ScenarioHost, onResult: OnResult) { 92 | val callback = { it: ActivityResult -> 93 | val result = if (it.resultCode == Activity.RESULT_OK) { 94 | "ABC" 95 | } else { 96 | "CANCELLED" 97 | } 98 | onResult.continueWith(result) 99 | } 100 | caller = when (host) { 101 | is ActivityHost -> host.activity.registerRouteForResult(ResultPickerRoute, callback) 102 | is FragmentHost -> host.fragment.registerRouteForResult(ResultPickerRoute, callback) 103 | } 104 | } 105 | 106 | override fun onHandleProcess(result: ActivityResult, lastResult: String?, onResult: OnResult) { 107 | if (result.resultCode == Activity.RESULT_OK) { 108 | caller(AppRouter) 109 | } else { 110 | onResult.continueWith("CANCELLED") 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/picker/ResultPickerScreen.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples.picker 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import kotlinx.android.synthetic.main.activity_result_picker.* 8 | import nolambda.linkrouter.examples.R 9 | 10 | class ResultPickerScreen : AppCompatActivity(R.layout.activity_result_picker) { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | btnResult.setOnClickListener { 15 | val data = Intent().apply { 16 | putExtra("result", "abc") 17 | } 18 | setResult(Activity.RESULT_OK, data) 19 | finish() 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /samples/app/src/main/java/nolambda/linkrouter/examples/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package nolambda.linkrouter.examples.utils 2 | 3 | import android.content.Context 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.PackageManager 6 | 7 | fun Context.isDebuggable(): Boolean { 8 | val pm: PackageManager = packageManager 9 | return try { 10 | val appInfo: ApplicationInfo = pm.getApplicationInfo(packageName, 0) 11 | 0 != appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE 12 | } catch (e: PackageManager.NameNotFoundException) { 13 | false 14 | } 15 | } -------------------------------------------------------------------------------- /samples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /samples/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /samples/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /samples/app/src/main/res/layout/activity_main_simple.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 |