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>
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
/** Project - Core */
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.decompose)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.core.ui.message
|
||||
|
||||
import androidx.compose.material.SnackbarDuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.ui.UiMessage
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* Event that is used to show a message in the UI.
|
||||
*
|
||||
* @see EventMessageHandler
|
||||
* */
|
||||
@Immutable
|
||||
sealed interface EventMessage : UiMessage
|
||||
|
||||
/**
|
||||
* Shows a snackbar.
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param duration The duration of the snackbar.
|
||||
* @param actionLabel The label of the action button.
|
||||
* @param action The action to perform when the action button is clicked.
|
||||
* */
|
||||
data class SnackbarMessage(
|
||||
val message: TextReference,
|
||||
val duration: SnackbarDuration = SnackbarDuration.Short,
|
||||
val actionLabel: TextReference? = null,
|
||||
val action: (() -> Unit)? = null,
|
||||
) : EventMessage
|
||||
|
||||
/**
|
||||
* Shows a [content] in the UI.
|
||||
*
|
||||
* @param content The content to show.
|
||||
* */
|
||||
data class ContentMessage(val content: Content) : EventMessage {
|
||||
|
||||
@Stable
|
||||
fun interface Content {
|
||||
|
||||
@Suppress("ComposableFunctionName", "TopLevelComposableFunctions")
|
||||
@Composable
|
||||
operator fun invoke(onDismiss: () -> Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.core.ui.message
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material.SnackbarResult
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
||||
@Composable
|
||||
fun EventMessageEffect(messageHandler: EventMessageHandler, snackbarHostState: SnackbarHostState) {
|
||||
val messageEvent by messageHandler.collectAsState()
|
||||
val context = LocalContext.current
|
||||
var contentMessage: ContentMessage? by remember { mutableStateOf(value = null) }
|
||||
|
||||
EventEffect(event = messageEvent) { message ->
|
||||
when (message) {
|
||||
is ContentMessage -> {
|
||||
contentMessage = message
|
||||
}
|
||||
is SnackbarMessage -> {
|
||||
showSnackbar(snackbarHostState, message, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentMessage?.content?.invoke(
|
||||
onDismiss = { contentMessage = null },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun showSnackbar(snackbarHostState: SnackbarHostState, message: SnackbarMessage, context: Context) {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = message.message.resolveReference(context.resources),
|
||||
actionLabel = message.actionLabel?.resolveReference(context.resources),
|
||||
duration = message.duration,
|
||||
)
|
||||
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
message.action?.invoke()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.core.ui.message
|
||||
|
||||
import com.tangem.core.decompose.ui.UiMessage
|
||||
import com.tangem.core.decompose.ui.UiMessageHandler
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Message handler that is used to show or remove an [EventMessage] in the UI.
|
||||
*/
|
||||
class EventMessageHandler(
|
||||
private val events: MutableStateFlow<StateEvent<EventMessage>>,
|
||||
) : UiMessageHandler, StateFlow<StateEvent<EventMessage>> by events {
|
||||
|
||||
override fun handleMessage(message: UiMessage) {
|
||||
if (message !is EventMessage) return
|
||||
|
||||
events.value = triggeredEvent(message, ::consumeEvent)
|
||||
}
|
||||
|
||||
private fun consumeEvent() {
|
||||
events.value = consumedEvent()
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import javax.inject.Inject
|
|||
|
||||
interface CoroutineDispatcherProvider {
|
||||
val main: CoroutineDispatcher
|
||||
val mainImmediate: CoroutineDispatcher
|
||||
val io: CoroutineDispatcher
|
||||
val default: CoroutineDispatcher
|
||||
val single: CoroutineDispatcher
|
||||
|
|
@ -15,6 +16,7 @@ interface CoroutineDispatcherProvider {
|
|||
|
||||
class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider {
|
||||
override val main: CoroutineDispatcher = Dispatchers.Main
|
||||
override val mainImmediate: CoroutineDispatcher = Dispatchers.Main.immediate
|
||||
override val io: CoroutineDispatcher = Dispatchers.IO
|
||||
override val default: CoroutineDispatcher = Dispatchers.Default
|
||||
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
|
||||
|
|
@ -22,6 +24,7 @@ class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcher
|
|||
|
||||
class TestingCoroutineDispatcherProvider(
|
||||
override val main: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val mainImmediate: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val io: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val default: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(),
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ swipeRefreshLayout = "1.1.0"
|
|||
spr-client = "3.6.2"
|
||||
web3j = "4.10.1"
|
||||
leakcanary = "2.13"
|
||||
decompose = "2.2.2"
|
||||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
|
|
@ -252,4 +253,6 @@ camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref =
|
|||
camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" }
|
||||
web3j-core = { module = "org.web3j:core", version.ref = "web3j" }
|
||||
leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
|
||||
decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
|
||||
decompose-ext-compose = { module = "com.arkivanov.decompose:extensions-compose-jetpack", version.ref = "decompose" }
|
||||
# endregion Other
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ include(":core:ui")
|
|||
include(":core:utils")
|
||||
include(":core:deep-links")
|
||||
include(":core:deep-links:global")
|
||||
include(":core:decompose")
|
||||
// endregion Core modules
|
||||
|
||||
// region Libs modules
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue