From 427024d0952abc53ceea492ca119cf24fcfb47ab Mon Sep 17 00:00:00 2001 From: Grigorii Date: Wed, 22 Mar 2023 15:57:37 +0400 Subject: [PATCH 1/3] Create deeplink_utils module --- deeplink-utils/.gitignore | 1 + deeplink-utils/build.gradle | 15 ++++ deeplink-utils/readme.md | 52 +++++++++++++ deeplink-utils/src/main/AndroidManifest.xml | 1 + .../deeplink_utils/DeeplinkHandler.kt | 73 +++++++++++++++++++ .../roboswag/deeplink_utils/DeeplinkModel.kt | 19 +++++ 6 files changed, 161 insertions(+) create mode 100644 deeplink-utils/.gitignore create mode 100644 deeplink-utils/build.gradle create mode 100644 deeplink-utils/readme.md create mode 100644 deeplink-utils/src/main/AndroidManifest.xml create mode 100644 deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkHandler.kt create mode 100644 deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkModel.kt diff --git a/deeplink-utils/.gitignore b/deeplink-utils/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/deeplink-utils/.gitignore @@ -0,0 +1 @@ +/build diff --git a/deeplink-utils/build.gradle b/deeplink-utils/build.gradle new file mode 100644 index 0000000..b7f6ef2 --- /dev/null +++ b/deeplink-utils/build.gradle @@ -0,0 +1,15 @@ +apply from: "../android-configs/lib-config.gradle" + +dependencies { + def coroutinesVersion = '1.6.4' + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") + + constraints { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") { + version { + require(coroutinesVersion) + } + } + } +} diff --git a/deeplink-utils/readme.md b/deeplink-utils/readme.md new file mode 100644 index 0000000..57e9fcd --- /dev/null +++ b/deeplink-utils/readme.md @@ -0,0 +1,52 @@ + +# Сущность для централизованнной обработки deeplink'ов + +### Поддерживает +- Добавление диплинка в очередь +- Очистка очереди и отмена всех активных операций +- Асинхронная обработка диплинка +- Цепочка обработчиков для диплинка +- Поиск обработчика в иерархии экранов + +### Использование + +Корневая навигация + +```kotlin +DeepLinkHandler + .observeDeeplink( + observerOrder = 0, + navigateAction = { deeplink -> + binding.bottomNavigation.selectedItemId = deeplink.getBottomNavigationItem().menuId + }) + .launchIn(viewLifecycleOwner.lifecycleScope) +``` + + +Навигация на втором уровне + +```kotlin +DeepLinkHandler + .observeDeeplink( + observerOrder = 1, + canHandle = { it.getBottomNavigationItem() == args.tabType }, + isFinalObserver = { !it.isListDeeplink() }, + navigateAction = { it.navigateToListScreen() } + ) + .launchIn(viewLifecycleOwner.lifecycleScope) +``` + +Навигация на третьем уровне + +```kotlin +DeepLinkHandler + .observeDeeplink( + observerOrder = 2, + canHandle = { it.getBottomNavigationItem() == args.tabType && it.isListDeeplink() }, + navigateCondition = { it.getItemId() != null }, + navigateAction = { it.navigateToDetailsScreen() } + ) + .launchIn(viewLifecycleOwner.lifecycleScope) +``` + +Пример проекта можно посмотреть в: https://github.com/duwna/Deeplinks-test \ No newline at end of file diff --git a/deeplink-utils/src/main/AndroidManifest.xml b/deeplink-utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000..623cf90 --- /dev/null +++ b/deeplink-utils/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkHandler.kt b/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkHandler.kt new file mode 100644 index 0000000..6de0f78 --- /dev/null +++ b/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkHandler.kt @@ -0,0 +1,73 @@ +package ru.touchin.roboswag.deeplink_utils + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +/** + * Support observing deepliks with different conditions with queue and cancellation. + * Must be used as Singleton for the App. + * */ +class DeeplinkHandler { + + /** + * @param observerOrder - entity index that can handle deeplink + * e.g. 0 - can be root bottom navigator, 1 - first screen of feature, 2 - second ... + * */ + class CurrentDeeplink( + val observerOrder: Int, + val deeplink: DeeplinkModel + ) + + private val currentDeeplink = MutableStateFlow(null) + private val deeplinkQueue = ArrayDeque() + + fun sendDeeplink(url: String) { + val deeplink = DeeplinkModel(url) + deeplinkQueue.add(deeplink) + + if (currentDeeplink.value == null) { + currentDeeplink.value = CurrentDeeplink(observerOrder = 0, deeplink = deeplink) + } + } + + fun observeDeeplink( + observerOrder: Int = 0, + canHandle: (DeeplinkModel) -> Boolean = { true }, + isFinalObserver: (DeeplinkModel) -> Boolean = { true }, + navigateCondition: (DeeplinkModel) -> Boolean = { true }, + navigateAction: suspend (DeeplinkModel) -> Unit = { } + ): Flow = + currentDeeplink + .filterNotNull() + .filter { it.observerOrder == observerOrder } + .map { it.deeplink } + .filter { canHandle(it) } + .onEach { if (navigateCondition(it) && currentDeeplink.value != null) navigateAction(it) } + .onEach { if (isFinalObserver(it)) markDeeplinkHandled() else it.sendToNextObserver(observerOrder) } + + fun cancel(deeplink: DeeplinkModel) { + deeplinkQueue.remove(deeplink) + if (currentDeeplink.value?.deeplink == deeplink) currentDeeplink.value = null + } + + fun cancelAll() { + deeplinkQueue.clear() + currentDeeplink.value = null + } + + private fun markDeeplinkHandled() { + deeplinkQueue.removeFirstOrNull() + + currentDeeplink.value = deeplinkQueue.firstOrNull()?.let { + CurrentDeeplink(observerOrder = 0, deeplink = it) + } + } + + private fun DeeplinkModel.sendToNextObserver(order: Int) { + currentDeeplink.value = CurrentDeeplink(observerOrder = order + 1, deeplink = this) + } +} diff --git a/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkModel.kt b/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkModel.kt new file mode 100644 index 0000000..fb6b30f --- /dev/null +++ b/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkModel.kt @@ -0,0 +1,19 @@ +package ru.touchin.roboswag.deeplink_utils + +import java.net.URL + +data class DeeplinkModel( + val url: String +) { + + private val parsedUrl by lazy { runCatching { URL(url) }.getOrNull() } + + val host: String? get() = parsedUrl?.host + val path: String? get() = parsedUrl?.path + val query: String? get() = parsedUrl?.query + + val partsParts: List? + get() = parsedUrl?.path + ?.split("/") + ?.filter { it.isNotEmpty() } +} From 023df551444667dc441119791017e2cd2775aa07 Mon Sep 17 00:00:00 2001 From: Grigorii Date: Fri, 24 Mar 2023 18:40:41 +0400 Subject: [PATCH 2/3] fix naming and docs --- .../touchin/roboswag/deeplink_utils/DeeplinkHandler.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkHandler.kt b/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkHandler.kt index 6de0f78..031f2c5 100644 --- a/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkHandler.kt +++ b/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkHandler.kt @@ -14,8 +14,11 @@ import kotlinx.coroutines.flow.onEach class DeeplinkHandler { /** - * @param observerOrder - entity index that can handle deeplink - * e.g. 0 - can be root bottom navigator, 1 - first screen of feature, 2 - second ... + * @param observerOrder + * Entity index that can handle deeplink + * E.g. 0 - can be root bottom navigation + * 1 - first screen that handles chain of deeplink screens ("home" screen) + * 2 - second screen ("list" screen) * */ class CurrentDeeplink( val observerOrder: Int, @@ -25,7 +28,7 @@ class DeeplinkHandler { private val currentDeeplink = MutableStateFlow(null) private val deeplinkQueue = ArrayDeque() - fun sendDeeplink(url: String) { + fun handleDeeplink(url: String) { val deeplink = DeeplinkModel(url) deeplinkQueue.add(deeplink) From 8213f0914e7388b6fa340e1a03ba0954fdb70838 Mon Sep 17 00:00:00 2001 From: Grigorii Date: Thu, 30 Mar 2023 15:45:19 +0400 Subject: [PATCH 3/3] make parsedUrl public --- .../touchin/roboswag/deeplink_utils/DeeplinkModel.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkModel.kt b/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkModel.kt index fb6b30f..179e6c4 100644 --- a/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkModel.kt +++ b/deeplink-utils/src/main/java/ru/touchin/roboswag/deeplink_utils/DeeplinkModel.kt @@ -6,13 +6,11 @@ data class DeeplinkModel( val url: String ) { - private val parsedUrl by lazy { runCatching { URL(url) }.getOrNull() } + val parsedUrl: URL? by lazy(LazyThreadSafetyMode.NONE) { + runCatching { URL(url) }.getOrNull() + } - val host: String? get() = parsedUrl?.host - val path: String? get() = parsedUrl?.path - val query: String? get() = parsedUrl?.query - - val partsParts: List? + val pathParts: List? get() = parsedUrl?.path ?.split("/") ?.filter { it.isNotEmpty() }