Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-11 15:54:46 +04:00
parent 7a10191880
commit e4e18ffddd
11 changed files with 226 additions and 3 deletions

View file

@ -2,6 +2,7 @@ package com.tangem.tap
import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.navigation.share.ShareManager
@ -116,4 +117,6 @@ interface ApplicationEntryPoint {
fun getUrlOpener(): UrlOpener
fun getShareManager(): ShareManager
fun getAppRouter(): AppRouter
}

View file

@ -27,9 +27,13 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import by.kirich1409.viewbindingdelegate.viewBinding
import com.arkivanov.decompose.value.observe
import com.arkivanov.essenty.lifecycle.asEssentyLifecycle
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
@ -60,6 +64,7 @@ import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
@ -72,6 +77,8 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
@ -162,6 +169,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var stakingRouter: StakingRouter
@Inject
@RootAppComponentContext
internal lateinit var rootComponentContext: AppComponentContext
@Inject
internal lateinit var appRouterConfig: AppRouterConfig
@Inject
internal lateinit var routingComponentFactory: RoutingComponent.Factory
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
@ -188,6 +205,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
observeAppThemeModeUpdates()
setContentView(R.layout.activity_main)
installRouting()
initContent()
checkForNotificationPermission()
@ -199,6 +217,29 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
private fun installRouting() {
val routingComponent = routingComponentFactory.create(
context = rootComponentContext,
)
appRouterConfig.routerScope = lifecycleScope
appRouterConfig.componentRouter = routingComponent.router
routingComponent.stack.observe(lifecycle.asEssentyLifecycle()) { childStack ->
appRouterConfig.baclStack = childStack.backStack.map { it.configuration }
when (val child = childStack.active.instance) {
is RoutingComponent.Child.Initial -> Unit
is RoutingComponent.Child.LegacyFragment -> {
supportFragmentManager.showFragmentAllowingStateLoss(child.name, child.fragmentProvider)
}
is RoutingComponent.Child.LegacyIntent -> {
startActivity(child.intent)
}
}
}
}
private fun observeStateUpdates() {
viewModel.state
.flowWithLifecycle(lifecycle)

View file

@ -13,6 +13,7 @@ import com.tangem.LogFormat
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
@ -193,6 +194,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val shareManager
get() = entryPoint.getShareManager()
private val appRouter: AppRouter
get() = entryPoint.getAppRouter()
// endregion
override fun onCreate() {
@ -284,6 +288,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
detailsEntryPoint = detailsEntryPoint,
urlOpener = urlOpener,
shareManager = shareManager,
appRouter = appRouter,
),
),
)

View file

@ -28,9 +28,53 @@ import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import timber.log.Timber
fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider: Provider<Fragment>) {
Timber.d("Showing $name route")
val isPoppedBack = popBackStackImmediate(name, 0)
if (!isPoppedBack) {
val fragment = fragmentProvider()
if (fragment is DialogFragment) {
fragment.showDialog(fragmentManager = this, name)
} else {
fragment.showFragment(fragmentManager = this, name)
}
Timber.d("Route $name is shown")
} else {
Timber.d("Route $name is found in backstack and shown")
}
}
private fun DialogFragment.showDialog(fragmentManager: FragmentManager, name: String) {
val transaction = fragmentManager.beginTransaction()
try {
transaction.addToBackStack(name)
show(transaction, name)
} catch (e: IllegalStateException) {
transaction.add(this, name)
transaction.addToBackStack(name)
transaction.commitAllowingStateLoss()
}
}
private fun Fragment.showFragment(fragmentManager: FragmentManager, name: String) {
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, this, name)
transaction.addToBackStack(name)
transaction.commitAllowingStateLoss()
}
fun FragmentActivity.openFragment(
screen: AppScreen,
addToBackstack: Boolean,

View file

@ -0,0 +1,26 @@
package com.tangem.tap.di.routing
import com.tangem.common.routing.AppRouter
import com.tangem.tap.routing.ProxyAppRouter
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.configurator.MutableAppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AppRouterModule {
@Provides
@Singleton
fun provideAppRouter(config: AppRouterConfig, dispatchers: CoroutineDispatcherProvider): AppRouter =
ProxyAppRouter(config, dispatchers)
@Provides
@Singleton
fun provideAppRouterConfigurator(): AppRouterConfig = MutableAppRouterConfig()
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.proxy.redux
import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
@ -85,4 +86,5 @@ data class DaggerGraphState(
val stakingRouter: StakingRouter? = null,
val urlOpener: UrlOpener? = null,
val shareManager: ShareManager? = null,
val appRouter: AppRouter? = null,
) : StateType

View file

@ -0,0 +1,61 @@
package com.tangem.tap.routing
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.navigation.Router
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.reflect.KClass
internal class ProxyAppRouter(
private val config: AppRouterConfig,
private val dispatchers: CoroutineDispatcherProvider,
) : AppRouter {
private val routerScope: CoroutineScope
get() = requireNotNull(config.routerScope) {
"Router scope is not set in config"
}
private val innerRouter: Router
get() = requireNotNull(config.componentRouter) {
"Inner router is not set in config"
}
override val stack: List<AppRoute>
get() = requireNotNull(config.stack) {
"Stack is not set in config"
}
override fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) {
routerScope.launch(dispatchers.mainImmediate) {
innerRouter.push(route, onComplete)
}
}
override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) {
routerScope.launch(dispatchers.mainImmediate) {
innerRouter.replaceAll(*routes, onComplete = onComplete)
}
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
routerScope.launch(dispatchers.mainImmediate) {
innerRouter.pop(onComplete)
}
}
override fun popTo(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) {
routerScope.launch(dispatchers.mainImmediate) {
innerRouter.popTo(route, onComplete)
}
}
override fun popTo(routeClass: KClass<out AppRoute>, onComplete: (isSuccess: Boolean) -> Unit) {
routerScope.launch(dispatchers.mainImmediate) {
innerRouter.popTo(routeClass, onComplete)
}
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.routing.configurator
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.navigation.Router
import kotlinx.coroutines.CoroutineScope
internal interface AppRouterConfig {
var routerScope: CoroutineScope?
var componentRouter: Router?
var stack: List<AppRoute>?
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.routing.configurator
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.navigation.Router
import kotlinx.coroutines.CoroutineScope
internal class MutableAppRouterConfig : AppRouterConfig {
override var routerScope: CoroutineScope? = null
override var componentRouter: Router? = null
override var stack: List<AppRoute>? = null
}

View file

@ -17,9 +17,10 @@ dependencies {
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models)
/* Libs - Other */
api(deps.kotlin.serialization)
implementation(deps.androidx.core.ktx)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
}

View file

@ -15,7 +15,7 @@ interface AppRouter {
/**
* The current navigation stack.
*/
val backStack: List<AppRoute>
val stack: List<AppRoute>
/**
* Pushes a new route to the navigation stack.
@ -25,6 +25,14 @@ interface AppRouter {
*/
fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Replaces ***all*** routes in the navigation stack with the specified [routes].
*
* @param routes The routes to replace the current stack with.
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Pops the top route from the navigation stack.
*
@ -33,7 +41,15 @@ interface AppRouter {
fun pop(onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Pops routes from the navigation stack until the specified route class is found.
* 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: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Pops routes from the navigation stack until the ***first*** specified [routeClass] is found.
*
* @param routeClass The route class to pop to.
* @param onComplete The callback to be invoked when the operation is complete.