英文:
How do I cancel an event after another one has started?
问题
我遇到一个问题,即在Snackbar出现后,FAB在Snackbar消息的时间段内不执行导航。如果我在Snackbar显示时多次点击它,它会堆叠并打开多个相同屏幕的实例(仅在Snackbar消失后)。如何在点击FAB时取消显示Snackbar并在任何时候保留FAB的功能?
TasksList.kt:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TasksListScreen(
modifier: Modifier = Modifier,
onNavigate: (UiEvent.Navigate) -> Unit,
viewModel: TaskListViewModel = hiltViewModel()
) {
val tasks = viewModel.tasks.collectAsState(initial = emptyList())
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
is UiEvent.ShowSnackbar -> {
val result = snackbarHostState.showSnackbar(
message = event.message,
actionLabel = event.action,
duration = SnackbarDuration.Long,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.onEvent(TaskListEvent.OnUndoDeleteTask)
}
}
is UiEvent.Navigate -> onNavigate(event)
else -> Unit
}
}
}
Scaffold(
snackbarHost = {
SnackbarHost(snackbarHostState) { data ->
Snackbar(
shape = RoundedShapes.medium,
actionColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.background,
snackbarData = data
)
}
},
floatingActionButton = {
FloatingActionButton(
shape = RoundedShapes.medium,
onClick = { viewModel.onEvent(TaskListEvent.OnAddTask) },
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.background
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(R.string.fab_cd)
)
}
},
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.background
)
}
},
) { padding ->
LazyColumn(
state = rememberLazyListState(),
verticalArrangement = spacedBy(12.dp),
contentPadding = PaddingValues(vertical = 16.dp),
modifier = modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp),
) {
items(items = tasks.value, key = { task -> task.hashCode() }) { task ->
val currentTask by rememberUpdatedState(newValue = task)
val dismissState = rememberDismissState(confirmValueChange = {
if (it == DismissValue.DismissedToStart) {
viewModel.onEvent(TaskListEvent.OnDeleteTask(currentTask))
}
true
})
SwipeToDismiss(state = dismissState,
directions = setOf(DismissDirection.EndToStart),
background = { },
dismissContent = {
TaskItem(
task = task, modifier = modifier
)
})
}
}
}
}
TaskListViewModel:
@HiltViewModel
class TaskListViewModel @Inject constructor(private val repository: TaskRepositoryImplementation) :
ViewModel() {
val tasks = repository.getTasks()
private val _uiEvent = Channel<UiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()
private var deletedTask: Task? = null
fun onEvent(event: TaskListEvent) {
when (event) {
is TaskListEvent.OnAddTask -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TASK))
}
is TaskListEvent.OnEditClick -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TASK + "?taskId=${event.task.id}"))
}
is TaskListEvent.OnDeleteTask -> {
val context = TaskApp.instance?.context
viewModelScope.launch(Dispatchers.IO) {
deletedTask = event.task
repository.deleteTask(event.task)
if (context != null) {
sendUiEvent(
UiEvent.ShowSnackbar(
message = context.getString(R.string.snackbar_deleted),
action = context.getString(R.string.snackbar_action)
)
)
}
}
}
is TaskListEvent.OnUndoDeleteTask -> {
deletedTask?.let { task ->
viewModelScope.launch {
repository.addTask(task)
}
}
}
}
}
private fun sendUiEvent(event: UiEvent) {
viewModelScope.launch(Dispatchers.IO) {
_uiEvent.send(event)
}
}
}
英文:
I am having an issue, where after a Snackbar appears the FAB won't perform navigation for the period of the Snackbar message. If I click it multiple times when the Snackbar is displayed it will stack and open multiple instances of one screen (only after the Snackbar is gone). How do I cancel showing the Snackbar on click of the FAB and preserve the functionality of the FAB at all times?
TasksList.kt:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TasksListScreen(
modifier: Modifier = Modifier,
onNavigate: (UiEvent.Navigate) -> Unit,
viewModel: TaskListViewModel = hiltViewModel()
) {
val tasks = viewModel.tasks.collectAsState(initial = emptyList())
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
is UiEvent.ShowSnackbar -> {
val result = snackbarHostState.showSnackbar(
message = event.message,
actionLabel = event.action,
duration = SnackbarDuration.Long,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.onEvent(TaskListEvent.OnUndoDeleteTask)
}
}
is UiEvent.Navigate -> onNavigate(event)
else -> Unit
}
}
}
Scaffold(
snackbarHost = {
SnackbarHost(snackbarHostState) { data ->
Snackbar(
shape = RoundedShapes.medium,
actionColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.background,
snackbarData = data
)
}
},
floatingActionButton = {
FloatingActionButton(
shape = RoundedShapes.medium,
onClick = { viewModel.onEvent(TaskListEvent.OnAddTask) },
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.background
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(R.string.fab_cd)
)
}
},
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.background
)
)
},
) { padding ->
LazyColumn(
state = rememberLazyListState(),
verticalArrangement = spacedBy(12.dp),
contentPadding = PaddingValues(vertical = 16.dp),
modifier = modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp),
) {
items(items = tasks.value, key = { task -> task.hashCode() }) { task ->
val currentTask by rememberUpdatedState(newValue = task)
val dismissState = rememberDismissState(confirmValueChange = {
if (it == DismissValue.DismissedToStart) {
viewModel.onEvent(TaskListEvent.OnDeleteTask(currentTask))
}
true
})
SwipeToDismiss(state = dismissState,
directions = setOf(DismissDirection.EndToStart),
background = { },
dismissContent = {
TaskItem(
task = task, modifier = modifier
)
})
}
}
}
}
TaskListViewModel:
@HiltViewModel
class TaskListViewModel @Inject constructor(private val repository: TaskRepositoryImplementation) :
ViewModel() {
val tasks = repository.getTasks()
private val _uiEvent = Channel<UiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()
private var deletedTask: Task? = null
fun onEvent(event: TaskListEvent) {
when (event) {
is TaskListEvent.OnAddTask -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TASK))
}
is TaskListEvent.OnEditClick -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TASK + "?taskId=${event.task.id}"))
}
is TaskListEvent.OnDeleteTask -> {
val context = TaskApp.instance?.context
viewModelScope.launch(Dispatchers.IO) {
deletedTask = event.task
repository.deleteTask(event.task)
if (context != null) {
sendUiEvent(
UiEvent.ShowSnackbar(
message = context.getString(R.string.snackbar_deleted),
action = context.getString(R.string.snackbar_action)
)
)
}
}
}
is TaskListEvent.OnUndoDeleteTask -> {
deletedTask?.let { task ->
viewModelScope.launch {
repository.addTask(task)
}
}
}
}
}
private fun sendUiEvent(event: UiEvent) {
viewModelScope.launch(Dispatchers.IO) {
_uiEvent.send(event)
}
}
}
答案1
得分: 2
以下是您要的代码的中文翻译:
// 创建SnackbarController类以创建可取消的Snackbar
// 受此存储库启发 [SnackbarController][1]
// 公开方法**cancelActiveJob**以取消可见的Snackbar
class SnackBarController(private val coroutineScope: CoroutineScope) {
private var snackBarJob: Job? = null
init {
cancelActiveJob()
}
suspend fun showSnackBar(
snackBarHostState: SnackbarHostState,
message: String,
actionLabel: String? = null,
duration: SnackbarDuration
): SnackbarResult {
var result: SnackbarResult = SnackbarResult.Dismissed
if (snackBarJob == null) {
snackBarJob = coroutineScope.launch {
result = snackBarHostState.showSnackbar(
message = message,
actionLabel = actionLabel,
duration = duration
)
cancelActiveJob()
}
} else {
cancelActiveJob()
snackBarJob = coroutineScope.launch {
result = snackBarHostState.showSnackbar(
message = message,
actionLabel = actionLabel,
duration = duration
)
cancelActiveJob()
}
}
snackBarJob?.join()
return result
}
fun cancelActiveJob() {
snackBarJob?.let { job ->
job.cancel()
snackBarJob = Job()
}
}
}
在TaskListScreen
中:
val tasks = viewModel.tasks.collectAsState(initial = emptyList())
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val snackBarController = SnackBarController(coroutineScope)
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collectLatest { event ->
when (event) {
is UiEvent.ShowSnackbar -> {
val result = snackBarController.showSnackBar(
message = event.message,
actionLabel = event.action,
duration = SnackbarDuration.Long,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.onEvent(TaskListEvent.OnUndoDeleteTask)
}
}
is UiEvent.Navigate -> {
// 如果要隐藏Snackbar
snackBarController.cancelActiveJob()
onNavigate(event)
}
else -> Unit
}
}
}
注意:如果多次调用snackbarController.showSnackBar()
,它会替换当前可见的Snackbar。
<details>
<summary>英文:</summary>
Create this SnackbarController class to create a cancellable snackbar
Inspired by this repo [SnackbarController][1]
Expose the method **cancelActiveJob** to cancel the visible snackbar
class SnackBarController(private val coroutineScope: CoroutineScope) {
private var snackBarJob: Job? = null
init {
cancelActiveJob()
}
suspend fun showSnackBar(
snackBarHostState: SnackbarHostState,
message: String,
actionLabel: String? = null,
duration: SnackbarDuration
): SnackbarResult {
var result: SnackbarResult = SnackbarResult.Dismissed
if (snackBarJob == null) {
snackBarJob = coroutineScope.launch {
result = snackBarHostState.showSnackbar(
message = message,
actionLabel = actionLabel,
duration = duration
)
cancelActiveJob()
}
} else {
cancelActiveJob()
snackBarJob = coroutineScope.launch {
result = snackBarHostState.showSnackbar(
message = message,
actionLabel = actionLabel,
duration = duration
)
cancelActiveJob()
}
}
snackBarJob?.join()
return result
}
fun cancelActiveJob() {
snackBarJob?.let { job ->
job.cancel()
snackBarJob = Job()
}
}
}
In the TaskListScreen
val tasks = viewModel.tasks.collectAsState(initial = emptyList())
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val snackBarController = SnackBarController(coroutineScope)
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collectLatest{ event ->
when (event) {
is UiEvent.ShowSnackbar -> {
val result = snackBarController.showSnackBar(
message = event.message,
actionLabel = event.action,
duration = SnackbarDuration.Long,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.onEvent(TaskListEvent.OnUndoDeleteTask)
}
}
is UiEvent.Navigate -> {
//if you want to hide the snackBar
snackBarController.cancelActiveJob()
onNavigate(event)
}
else -> Unit
}
}
}
Note if you call snackbarController.showSnackbar() more than once
It replaces the current visible snackbar
[1]: https://github.com/mitchtabian/food2fork-compose/blob/d291d5114a0a5dd09e7d5ab2de3804607cfbd5d2/app/src/main/java/com/codingwithmitch/food2forkcompose/presentation/components/util/SnackbarController.kt
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论