Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-13 09:56:02 +03:00
parent b97ddcb3f2
commit c6ddab601b
20 changed files with 370 additions and 160 deletions

View file

@ -11,4 +11,7 @@ android {
dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
implementation(deps.compose.foundation)
implementation(deps.compose.navigation)
}

View file

@ -1,8 +1,12 @@
package com.tangem.features.managetokens.navigation
import androidx.fragment.app.Fragment
import androidx.compose.ui.unit.Dp
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
interface ManageTokensRouter {
fun getEntryFragment(): Fragment
val startDestination: String
fun NavGraphBuilder.initialize(navController: NavHostController, onHeaderSizeChange: (Dp) -> Unit)
}

View file

@ -35,6 +35,7 @@ dependencies {
implementation(deps.jodatime)
implementation(deps.kotlin.immutable.collections)
implementation(deps.tangem.card.core)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)

View file

@ -1,37 +0,0 @@
package com.tangem.managetokens
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.features.managetokens.navigation.ManageTokensRouter
import com.tangem.managetokens.presentation.router.InnerManageTokensRouter
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class ManageTokensFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
lateinit var manageTokensRouter: ManageTokensRouter
private val innerManageTokensRouter: InnerManageTokensRouter
get() = requireNotNull(manageTokensRouter as? InnerManageTokensRouter) {
"internalManageTokensRouter should be instance of InnerManageTokensRouter"
}
@Composable
override fun ScreenContent(modifier: Modifier) {
val systemBarsColor = TangemTheme.colors.background.secondary
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
innerManageTokensRouter.Initialize(viewModelStoreOwner = this)
}
}

View file

@ -8,13 +8,18 @@ import androidx.compose.material.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SpacerH18
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.res.TangemTheme
@ -32,7 +37,7 @@ import com.tangem.managetokens.presentation.managetokens.ui.components.TokensLis
import com.tangem.managetokens.presentation.managetokens.ui.components.TokensSearchBar
@Composable
internal fun ManageTokensScreen(state: ManageTokensState) {
internal fun ManageTokensScreen(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit) {
var alertState by remember { mutableStateOf<AlertState?>(value = null) }
EventEffect(
@ -43,43 +48,50 @@ internal fun ManageTokensScreen(state: ManageTokensState) {
Alert(state = it, onDismiss = { alertState = null })
}
Content(state)
Content(state = state, onHeaderSizeChange = onHeaderSizeChange)
}
@Composable
private fun Content(state: ManageTokensState) {
private fun Content(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit) {
val keyboard by keyboardAsState()
val density = LocalDensity.current
var tokenListAlertBottomPadding by remember(keyboard is Keyboard.Opened) { mutableStateOf(0.dp) }
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding()
.imePadding()
.background(color = TangemTheme.colors.background.primary)
.padding(top = TangemTheme.dimens.spacing32),
.background(color = TangemTheme.colors.background.primary),
) {
Column {
val listState = rememberLazyListState()
val raiseSearchBar by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
val elevation by animateDpAsState(
targetValue = if (raiseSearchBar) {
TangemTheme.dimens.elevation8
} else {
TangemTheme.dimens.elevation0
},
targetValue = if (raiseSearchBar) TangemTheme.dimens.elevation8 else TangemTheme.dimens.elevation0,
label = "top_bar_elevation",
)
Surface(
elevation = elevation,
modifier = Modifier,
modifier = Modifier.onGloballyPositioned {
with(density) { onHeaderSizeChange(it.size.height.toDp()) }
},
) {
TokensSearchBar(
state = state.searchBarState,
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing20),
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing4,
),
)
}
SpacerH18()
val tokens = state.tokens.collectAsLazyPagingItems()
val query = state.searchBarState.query
@ -89,7 +101,15 @@ private fun Content(state: ManageTokensState) {
onEmptySearchResult = state.onEmptySearchResult,
)
TokensList(tokens = tokens, addCustomTokenButton = state.addCustomTokenButton)
TokensList(
modifier = Modifier.padding(bottom = tokenListAlertBottomPadding),
tokens = tokens,
addCustomTokenButton = state.addCustomTokenButton,
)
}
state.selectedToken?.let { selectedToken ->
ManageTokensBottomSheet(selectedToken = selectedToken, state = state)
}
state.derivationNotification?.let {
@ -97,14 +117,13 @@ private fun Content(state: ManageTokensState) {
DerivationNotification(
config = it.config,
modifier = Modifier
.align(Alignment.BottomCenter),
.align(Alignment.BottomCenter)
.onGloballyPositioned {
with(density) { tokenListAlertBottomPadding = it.size.height.toDp() }
},
)
}
}
state.selectedToken?.let { selectedToken ->
ManageTokensBottomSheet(selectedToken = selectedToken, state = state)
}
}
}
@ -158,7 +177,7 @@ private fun Preview_ManageTokensScreen_LightTheme(
state: ManageTokensState,
) {
TangemTheme(isDark = false) {
ManageTokensScreen(state)
ManageTokensScreen(state) {}
}
}
@ -169,7 +188,7 @@ private fun Preview_ManageTokensScreen_DarkTheme(
state: ManageTokensState,
) {
TangemTheme(isDark = true) {
ManageTokensScreen(state)
ManageTokensScreen(state) {}
}
}

View file

@ -17,8 +17,12 @@ import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
private const val PLACEHOLDER_ITEMS_COUNT = 50
@Composable
internal fun TokensList(tokens: LazyPagingItems<TokenItemState>, addCustomTokenButton: AddCustomTokenButton) {
LazyColumn {
internal fun TokensList(
tokens: LazyPagingItems<TokenItemState>,
addCustomTokenButton: AddCustomTokenButton,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
Text(
text = stringResource(id = R.string.manage_tokens_title),

View file

@ -1,25 +1,22 @@
package com.tangem.managetokens.presentation.router
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.fragment.app.Fragment
import androidx.compose.ui.unit.Dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModelStoreOwner
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navigation
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.managetokens.ManageTokensFragment
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
import com.tangem.core.ui.extensions.parentHiltViewModel
import com.tangem.managetokens.presentation.addcustomtoken.ui.AddCustomTokenScreen
import com.tangem.managetokens.presentation.addcustomtoken.ui.ChooseDerivationScreen
import com.tangem.managetokens.presentation.addcustomtoken.ui.ChooseNetworkCustomScreen
import com.tangem.managetokens.presentation.addcustomtoken.ui.CustomTokensChooseWalletScreen
import com.tangem.managetokens.presentation.addcustomtoken.ui.AddCustomTokenScreen
import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenViewModel
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
import com.tangem.managetokens.presentation.managetokens.ui.ManageTokensScreen
import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensViewModel
import kotlin.properties.Delegates
@ -30,64 +27,69 @@ internal class DefaultManageTokensRouter(
private var navController: NavHostController by Delegates.notNull()
override fun getEntryFragment(): Fragment = ManageTokensFragment()
override val startDestination: String = ManageTokensRoute.ManageTokens.route
@Composable
override fun Initialize(viewModelStoreOwner: ViewModelStoreOwner) {
NavHost(
navController = rememberNavController().apply { navController = this },
startDestination = ManageTokensRoute.ManageTokens.route,
) {
composable(ManageTokensRoute.ManageTokens.route) {
val viewModel = hiltViewModel<ManageTokensViewModel>().apply { router = this@DefaultManageTokensRouter }
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
override fun NavGraphBuilder.initialize(navController: NavHostController, onHeaderSizeChange: (Dp) -> Unit) {
this@DefaultManageTokensRouter.navController = navController
ManageTokensScreen(state = viewModel.uiState)
composable(ManageTokensRoute.ManageTokens.route) {
val viewModel = hiltViewModel<ManageTokensViewModel>().apply {
router = this@DefaultManageTokensRouter
}
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
navigation(
startDestination = ManageTokensRoute.AddCustomToken.Main.route,
route = ManageTokensRoute.AddCustomToken.route,
) {
composable(
ManageTokensRoute.AddCustomToken.Main.route,
) {
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
ManageTokensScreen(
state = viewModel.uiState,
onHeaderSizeChange = onHeaderSizeChange,
)
}
navigation(
route = ManageTokensRoute.AddCustomToken.route,
startDestination = ManageTokensRoute.AddCustomToken.Main.route,
) {
composable(
ManageTokensRoute.AddCustomToken.Main.route,
) { backStackEntry ->
val viewModel =
backStackEntry.parentHiltViewModel<AddCustomTokenViewModel>(navController = navController).apply {
router = this@DefaultManageTokensRouter
}
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
AddCustomTokenScreen(state = viewModel.uiState)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseNetwork.route,
) {
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
AddCustomTokenScreen(state = viewModel.uiState)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseNetwork.route,
) { backStackEntry ->
val viewModel =
backStackEntry.parentHiltViewModel<AddCustomTokenViewModel>(navController = navController).apply {
router = this@DefaultManageTokensRouter
}
ChooseNetworkCustomScreen(
state = viewModel.uiState.chooseNetworkState,
)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseDerivation.route,
) {
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
ChooseNetworkCustomScreen(
state = viewModel.uiState.chooseNetworkState,
)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseDerivation.route,
) { backStackEntry ->
val viewModel =
backStackEntry.parentHiltViewModel<AddCustomTokenViewModel>(navController = navController).apply {
router = this@DefaultManageTokensRouter
}
ChooseDerivationScreen(
state = requireNotNull(viewModel.uiState.chooseDerivationState),
)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseWallet.route,
) {
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
ChooseDerivationScreen(
state = requireNotNull(viewModel.uiState.chooseDerivationState),
)
}
composable(
ManageTokensRoute.AddCustomToken.ChooseWallet.route,
) { backStackEntry ->
val viewModel =
backStackEntry.parentHiltViewModel<AddCustomTokenViewModel>(navController = navController).apply {
router = this@DefaultManageTokensRouter
}
CustomTokensChooseWalletScreen(
state = viewModel.uiState.chooseWalletState as ChooseWalletState.Choose,
)
}
CustomTokensChooseWalletScreen(
state = viewModel.uiState.chooseWalletState as ChooseWalletState.Choose,
)
}
}
}

View file

@ -1,19 +1,10 @@
package com.tangem.managetokens.presentation.router
import androidx.compose.runtime.Composable
import androidx.lifecycle.ViewModelStoreOwner
import com.tangem.core.navigation.AppScreen
import com.tangem.features.managetokens.navigation.ManageTokensRouter
internal interface InnerManageTokensRouter : ManageTokensRouter {
/**
* Initialize router
**/
@Suppress("TopLevelComposableFunctions")
@Composable
fun Initialize(viewModelStoreOwner: ViewModelStoreOwner)
/** Pop back stack */
fun popBackStack(screen: AppScreen? = null)