Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-17 12:37:49 +03:00
parent c66a6f7ecb
commit cf59159d55
32 changed files with 681 additions and 0 deletions

1
core/decompose/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,15 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
dependencies {
implementation(projects.core.utils)
api(deps.decompose)
implementation(deps.kotlin.coroutines)
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,23 @@
package com.tangem.core.decompose.context
import com.arkivanov.decompose.ComponentContext
import com.tangem.core.decompose.di.HiltComponentBuilderOwner
import com.tangem.core.decompose.navigation.NavigationOwner
import com.tangem.core.decompose.ui.UiMessageSenderOwner
import com.tangem.core.decompose.utils.ComponentScopeOwner
import com.tangem.core.decompose.utils.DispatchersOwner
import com.tangem.core.decompose.utils.TagsOwner
/**
* Interface for the application component context.
*
* It combines several other interfaces related to navigation, dispatching, UI messaging, etc.
*/
interface AppComponentContext :
ComponentContext,
NavigationOwner,
ComponentScopeOwner,
DispatchersOwner,
UiMessageSenderOwner,
HiltComponentBuilderOwner,
TagsOwner

View file

@ -0,0 +1,73 @@
package com.tangem.core.decompose.context
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.childContext
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.tangem.core.decompose.di.HiltComponentBuilderOwner
import com.tangem.core.decompose.navigation.NavigationOwner
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageHandler
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.decompose.ui.UiMessageSenderOwner
import com.tangem.core.decompose.utils.ComponentCoroutineScope
import com.tangem.core.decompose.utils.DispatchersOwner
import kotlinx.coroutines.CoroutineScope
/**
* Creates a new child [AppComponentContext] with the provided [key] and optional [lifecycle].
*
* @param key The key to use.
* @param lifecycle The [Lifecycle] to use. If not provided, the parent's lifecycle will be used.
* @param router The [Router] to use in the child. If not provided, the parent's router will be used.
* @param messageHandler The [UiMessageHandler] to use in the child. If not provided, the parent's message sender will
* be used.
*
* @see childByContext
* */
fun AppComponentContext.child(
key: String,
lifecycle: Lifecycle? = null,
router: Router? = null,
messageHandler: UiMessageHandler? = null,
): AppComponentContext = childByContext(
componentContext = childContext(key, lifecycle),
router = router,
messageHandler = messageHandler,
)
/**
* Creates a new child [AppComponentContext] with the provided [componentContext].
*
* @param componentContext The [ComponentContext] to use.
* @param router The [Router] to use in the child. If not provided, the parent's router will be used.
* @param messageHandler The [UiMessageHandler] to use in the child. If not provided, the parent's message sender will
* be used.
*
* @see child
* */
fun AppComponentContext.childByContext(
componentContext: ComponentContext,
router: Router? = null,
messageHandler: UiMessageHandler? = null,
): AppComponentContext = object :
AppComponentContext,
ComponentContext by componentContext,
NavigationOwner by this@childByContext,
UiMessageSenderOwner by this@childByContext,
DispatchersOwner by this@childByContext,
HiltComponentBuilderOwner by this@childByContext {
override val tags: HashMap<String, Any> = HashMap()
override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers)
override val messageSender: UiMessageSender = messageHandler
?.let(::DefaultUiMessageSender)
?: this@childByContext.messageSender
override val router: Router
get() = router ?: this@childByContext.router
}

View file

@ -0,0 +1,35 @@
package com.tangem.core.decompose.context
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.navigation.AppNavigationProvider
import com.tangem.core.decompose.navigation.DefaultAppNavigationProvider
import com.tangem.core.decompose.navigation.DefaultRouter
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageHandler
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.decompose.utils.ComponentCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
class DefaultAppComponentContext(
componentContext: ComponentContext,
messageHandler: UiMessageHandler,
override val dispatchers: CoroutineDispatcherProvider,
override val hiltComponentBuilder: DecomposeComponent.Builder,
) : AppComponentContext, ComponentContext by componentContext {
override val tags: HashMap<String, Any> = HashMap()
override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers)
override val messageSender: UiMessageSender = DefaultUiMessageSender(messageHandler)
override val navigationProvider: AppNavigationProvider
get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() }
override val router: Router
get() = instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) }
}

View file

@ -0,0 +1,9 @@
package com.tangem.core.decompose.di
/**
* Annotation for marking a dependency as a component scoped.
*
* This means that the lifecycle of the dependency is limited to the lifecycle of the component it is attached to.
*/
@Retention(AnnotationRetention.SOURCE)
annotation class ComponentScoped

View file

@ -0,0 +1,47 @@
package com.tangem.core.decompose.di
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import dagger.BindsInstance
import dagger.hilt.DefineComponent
import dagger.hilt.components.SingletonComponent
/**
* Interface for the Decompose component.
*
* It is annotated as [ComponentScoped], meaning it has a lifecycle that is scoped to the component.
*/
@ComponentScoped
@DefineComponent(parent = SingletonComponent::class)
interface DecomposeComponent {
/**
* Builder interface for the component.
*/
@DefineComponent.Builder
interface Builder {
/**
* Sets the router for the component.
*
* @param router The router to set.
* @return The builder instance.
*/
fun router(@BindsInstance router: Router): Builder
/**
* Sets the UI message sender for the component.
*
* @param uiMessageSender The UI message sender to set.
* @return The builder instance.
*/
fun uiMessageSender(@BindsInstance uiMessageSender: UiMessageSender): Builder
/**
* Builds the Decompose component.
*
* @return The built Decompose component.
*/
fun build(): DecomposeComponent
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.decompose.di
/**
* Interface for owning a Hilt component builder.
*/
interface HiltComponentBuilderOwner {
/**
* Provides access to the Hilt component builder instance.
*/
val hiltComponentBuilder: DecomposeComponent.Builder
}

View file

@ -0,0 +1,36 @@
package com.tangem.core.decompose.model
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
/**
* Abstract class for a component's model.
*
* It provides access to the coroutine dispatchers and a coroutine scope which will survive re-creation of component
* and will be destroyed when the component is destroyed.
*
* Also, it can inject and use some component features like [Router] and [UiMessageSender].
*/
abstract class Model : InstanceKeeper.Instance {
/**
* Provides access to the coroutine dispatchers.
*/
protected abstract val dispatchers: CoroutineDispatcherProvider
/**
* The coroutine scope for the model. That will be cancelled when the model is destroyed.
*/
protected val modelScope by lazy {
CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob())
}
override fun onDestroy() {
runCatching { modelScope.cancel() }
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.core.decompose.model
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import javax.inject.Provider
/**
* Entry point for the models in the application.
*
* It provides a map of model providers.
*/
@EntryPoint
@InstallIn(DecomposeComponent::class)
interface ModelsEntryPoint {
fun models(): Map<Class<*>, Provider<Model>>
}
/**
* Gets or creates a component's [Model].
*/
inline fun <reified M : Model> AppComponentContext.getOrCreateModel(): M {
val modelKey = "model_${M::class.simpleName}"
val entryPoint = instanceKeeper.getOrCreateSimple(key = "modelsEntryPoint") {
val hiltComponent = hiltComponentBuilder
.router(router)
.uiMessageSender(messageSender)
.build()
EntryPoints.get(hiltComponent, ModelsEntryPoint::class.java)
}
val model = instanceKeeper.getOrCreate(modelKey) {
requireNotNull(entryPoint.models()[M::class.java]?.get()) {
"Model ${M::class.simpleName} is not provided"
}
}
val isModelExist = tags.getOrElse(modelKey) { false } as Boolean
if (!isModelExist) {
tags[modelKey] = true
}
return model as M
}

View file

@ -0,0 +1,28 @@
@file:Suppress("UNCHECKED_CAST")
package com.tangem.core.decompose.navigation
import com.arkivanov.decompose.router.stack.StackNavigation
/**
* Interface for providing application navigation.
* It provides or creates a StackNavigation instance for the application.
*/
interface AppNavigationProvider {
/**
* Gets or creates a StackNavigation instance.
*
* @return The StackNavigation instance.
*/
fun getOrCreate(): StackNavigation<Route>
}
/**
* Gets or creates a [StackNavigation] instance of a specific type.
*
* @return The [StackNavigation] instance.
*/
fun <R : Route> AppNavigationProvider.getOrCreateTyped(): StackNavigation<R> {
return getOrCreate() as StackNavigation<R>
}

View file

@ -0,0 +1,13 @@
package com.tangem.core.decompose.navigation
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
internal class DefaultAppNavigationProvider : AppNavigationProvider, InstanceKeeper.Instance {
private var navigation: StackNavigation<Route>? = null
override fun getOrCreate(): StackNavigation<Route> {
return navigation ?: StackNavigation<Route>().also { navigation = it }
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.core.decompose.navigation
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.popWhile
import com.arkivanov.decompose.router.stack.pushNew
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
internal class DefaultRouter(
private val navigationProvider: AppNavigationProvider,
) : Router, InstanceKeeper.Instance {
private val navigation: StackNavigation<Route>
get() = navigationProvider.getOrCreate()
@OptIn(ExperimentalDecomposeApi::class)
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
navigation.pushNew(route, onComplete)
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
navigation.pop(onComplete)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
navigation.popWhile(
predicate = { it != route },
onComplete = onComplete,
)
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.core.decompose.navigation
/**
* Interface for owning navigation-related properties.
*/
interface NavigationOwner {
/**
* The [Router] instance.
*/
val router: Router
/**
* The [AppNavigationProvider] instance.
*/
val navigationProvider: AppNavigationProvider
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.decompose.navigation
/**
* Interface for a route in the application.
*/
interface Route

View file

@ -0,0 +1,31 @@
package com.tangem.core.decompose.navigation
/**
* Interface for a router in the application.
* It provides methods for navigating through the application.
*/
interface Router {
/**
* Pushes a new route to the navigation stack.
*
* @param route The route to push.
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Pops the top route from the navigation stack.
*
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun pop(onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Pops routes from the navigation stack until the specified route is found.
*
* @param route The route to pop to.
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.decompose.ui
internal class DefaultUiMessageSender(
private val handler: UiMessageHandler,
) : UiMessageSender {
override fun send(message: UiMessage) {
handler.handleMessage(message)
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.core.decompose.ui
/**
* Interface for a message that can be sent to a [UiMessageSender] and handled by a [UiMessageHandler].
*
* @see UiMessageSender
* @see UiMessageHandler
*/
interface UiMessage

View file

@ -0,0 +1,17 @@
package com.tangem.core.decompose.ui
/**
* Interface for handling UI messages.
*
* @see UiMessage
* @see UiMessageSender
*/
interface UiMessageHandler {
/**
* Handles the given UI message.
*
* @param message The UI message to handle.
*/
fun handleMessage(message: UiMessage)
}

View file

@ -0,0 +1,17 @@
package com.tangem.core.decompose.ui
/**
* Interface for sending messages to UI.
*
* @see UiMessage
* @see UiMessageHandler
* */
interface UiMessageSender {
/**
* Sends the given UI message.
*
* @param message The UI message to send.
* */
fun send(message: UiMessage)
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.decompose.ui
/**
* Interface for owning a [UiMessageSender].
* */
interface UiMessageSenderOwner {
/**
* The [UiMessageSender] instance.
* */
val messageSender: UiMessageSender
}

View file

@ -0,0 +1,20 @@
package com.tangem.core.decompose.utils
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.doOnDestroy
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
/**
* [CoroutineDispatcherProvider.mainImmediate] dispatcher.
* */
@Suppress("FunctionName")
internal fun ComponentCoroutineScope(lifecycle: Lifecycle, dispatchers: CoroutineDispatcherProvider): CoroutineScope {
val scope = CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob())
lifecycle.doOnDestroy(scope::cancel)
return scope
}

View file

@ -0,0 +1,16 @@
package com.tangem.core.decompose.utils
import kotlinx.coroutines.CoroutineScope
/**
* Interface for owning a component scope.
*/
interface ComponentScopeOwner {
/**
* Provides access to the component's [CoroutineScope] instance.
*
* This scope is used for launching coroutines that are bound to the component's lifecycle.
*/
val componentScope: CoroutineScope
}

View file

@ -0,0 +1,14 @@
package com.tangem.core.decompose.utils
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* Interface for owning a [CoroutineDispatcherProvider].
*/
interface DispatchersOwner {
/**
* Provides access to the [CoroutineDispatcherProvider] instance.
*/
val dispatchers: CoroutineDispatcherProvider
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.decompose.utils
/**
* Interface for owning tags.
*/
interface TagsOwner {
/**
* Provides access to the tags map instance.
*/
val tags: HashMap<String, Any>
}