Updated on 2026-08-14
This commit is contained in:
parent
c66a6f7ecb
commit
cf59159d55
32 changed files with 681 additions and 0 deletions
1
core/decompose/.gitignore
vendored
Normal file
1
core/decompose/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
15
core/decompose/build.gradle.kts
Normal file
15
core/decompose/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.decompose.navigation
|
||||
|
||||
/**
|
||||
* Interface for a route in the application.
|
||||
*/
|
||||
interface Route
|
||||
|
|
@ -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 = {})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.decompose.ui
|
||||
|
||||
/**
|
||||
* Interface for owning a [UiMessageSender].
|
||||
* */
|
||||
interface UiMessageSenderOwner {
|
||||
|
||||
/**
|
||||
* The [UiMessageSender] instance.
|
||||
* */
|
||||
val messageSender: UiMessageSender
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue