[Разработка мобильных приложений, Разработка под Android, Kotlin, Дизайн мобильных приложений] Реализация Undo в Snackbar на Jetpack Compose (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Пользовательский опыт (UX — User Experience) - это то, как пользователи воспринимают продукт и какие впечатления получают от взаимодействия с ним.В процессе разработки мобильного приложения важно делать акцент на пользовательском опыте в ситуациях, когда от пользователя требуется подтверждение каких-либо действий (или, наоборот, отмена уже совершённого действия). Если приложение будет выводить AlertDialog по поводу и без, пользователю это вряд ли понравится.В библиотеке компонентов материального дизайна есть Snackbar — виджет, который используется для отображения сообщений в нижней части приложения.Чем хорош снэкбар:
- информирует пользователя о статусе процесса, который приложение выполнило или будет выполнять,
- не прерывает взаимодействие пользователя с приложением,
- может содержать дополнительную кнопку для программирования различных действий.
Используем SnackbarВ Jetpack Compose уже есть реализация снэкбара. В примере ниже он отображается после нажатия на кнопку:
@Composable
fun SnackbarSample() {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val modifier = Modifier
Box(modifier.fillMaxSize()) {
Button(onClick = {
coroutineScope.launch {
snackbarHostState.showSnackbar(message = "This is a Snackbar")
}
}) {
Text(text = "Click me!")
}
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
В коде два основных компонента: SnackbarHost и SnackbarHostState. Их использование позволяет правильно отображать, скрывать и закрывать снэкбар — в соответствии с гайдлайнами материального дизайна.Единовременно может отображаться один снэкбар — остальные будут ждать в очереди.Добавляем Undo в SnackbarРассмотрим пример посложнее. Реализуем снэкбар, который отменяет действие пользователя. В нашем примере будем отображать список задач и следить за его изменением. В этом нам поможет ViewModel.Упрощённый пример:
@Composable
fun HomeList(taskViewModel: ListViewModel = viewModel()) {
Scaffold {
val list by remember(taskViewModel) {
taskViewModel.taskList
}.collectAsState()
LazyColumn {
items(
items = list,
itemContent = { task ->
ListItem(
task = task,
onCheckedChange = taskViewModel::onCheckedChange
)
}
)
}
}
}
@Composable
private fun ListItem(task: Task, onCheckedChange: (Task) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = task.isCompleted, onCheckedChange = { onCheckedChange(task) })
Spacer(Modifier.width(8.dp))
Text(text = task.title)
}
}
Наша composable-функция получает на вход ViewModel, который, в свою очередь, подгружает список задач (viewModel.taskList) и вызывает функцию проверки их статуса (viewModel.onCheckedChange).Scaffold используется для построения правильной структуры лэйаута. Далее по тексту станет понятно, как она будет связана со снэкбаром. А пока мы имеем такой результат:
Наше приложение должно реализовывать следующее бизнес-правило: в списке отображаются только незавершённые задачи. После нажатия на чекбокс, задача должна сразу же удаляться из списка. За реализацию этой логики отвечает ViewModel.А теперь давайте реализуем функционал, который позволит пользователю отменять действие. Первым делом создадим лямбда-функцию, которая будет вызываться вместе со снэкбаром, когда пользователь нажимает на чекбокс.
@Composable
fun HomeList(taskViewModel: ListViewModel = viewModel()) {
val coroutineScope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
val onShowSnackbar: (Task) -> Unit = { task ->
coroutineScope.launch {
val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
message = "${task.title} completed",
actionLabel = "Undo"
)
when (snackbarResult) {
SnackbarResult.Dismissed -> Timber.d("Snackbar dismissed")
SnackbarResult.ActionPerformed -> taskViewModel.onCheckedChange(task)
}
}
}
...
}
Разберём код:
- Строка #3: сохраняем CoroutineScope (потребуется позже при показе снэкбара)
- Строка #4: сохраняем ScaffoldState, который содержит настроенный SnackbarHostState.
- Строка #6: вызываем лямбда-функцию, когда пользователь нажимает на чекбокс. В качестве входного параметра она получает задачу, выбранную в списке.
- Строка #8: вызываем suspend-функцию showSnackbar() и показываем снэкбар. Используя SnackbarResult, понимаем, какие произошли изменения (как изменилось состояние).
- Строки #12-14: обрабатываем два возможных результата (Dismissed и ActionPerformed). Если кнопку не нажали, пишем сообщение в лог. Если кнопку нажали, ViewModel меняет значение boolean-переменной isCompleted на противоположное (с false на true). Когда значение переменной isCompleted опять будет false, задача вернётся в список.
Описание ViewModel
data class Task(val id: Long, val title: String, var isCompleted: Boolean)
class ListViewModel : ViewModel() {
private val list = mutableListOf(
Task(1L, "Buy milk", false),
Task(2L, "Watch 'Call Me By Your Name'", false),
Task(3L, "Listen 'Local Natives'", false),
Task(4L, "Study about 'fakes instead of mocks'", false),
Task(5L, "Congratulate Rafael", false),
Task(6L, "Watch Kotlin YouTube Channel", false)
)
private val _taskList: MutableStateFlow<List<Task>> = MutableStateFlow(list)
val taskList: StateFlow<List<Task>>
get() = _taskList.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), listOf())
fun onCheckedChange(task: Task) {
list.find { it.id == task.id }?.isCompleted = task.isCompleted.not()
_taskList.value = list.filter { it.isCompleted.not() }
}
}
Теперь мы можем связать наш снэкбар с Scaffold и вызывать лямбда-функцию в качестве параметра в методе onCheckedChange:
Scaffold(scaffoldState = scaffoldState) {
...
ListItem(
task = task,
onCheckedChange = { task ->
taskViewModel.onCheckedChange(task)
onShowSnackbar(task)
}
)
}
Так как у ScaffoldState уже есть SnackbarHostState, который мы определили ранее, мы просто передаём его в виде параметра и получаем желаемый результат:
И ещё кое-чтоВ процессе разработки я столкнулся с тем, что после нажатия на чекбокс снэкбар появлялся и тут же исчезал. Решить её мне помог Адам Пауэлл на Kotlinlang в Slack.Проблема заключалась в следующем: снэкбар удалялся из очереди, когда вызов showSnackbar отменялся. Изначально я объявлял rememberCoroutineScope в compose-функции ListItem, которая формировала элементы списка задач. Проблему решил перенос её объявления выше Scaffold.Что дальше?Полный исходный код для это статьи доступен в этом gist-репозитории (и в конце статьи). Не пинайте сильно за реализацию ViewModel. Она примитивна, да, и она здесь лишь для имитации "живых данных". В реальном приложении данные будут поступать, например, из Flow, связанного с Room.Для своего приложения реализацию Undo в снэкбаре я добавил в этом пул-реквесте. Если есть желание, посмотрите.Надеюсь, эта статья поможет вам создавать свои реализации отмены действий в снэкбарах. Спасибо за внимание, а Адаму Пауэллу за помощь с корутинами.Полный исходный код
@Composable
fun HomeList(taskViewModel: ListViewModel = viewModel()) {
val coroutineScope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
val onShowSnackbar: (Task) -> Unit = { task ->
coroutineScope.launch {
val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
message = "${task.title} completed",
actionLabel = "Undo"
)
when (snackbarResult) {
SnackbarResult.Dismissed -> Timber.d("Snackbar dismissed")
SnackbarResult.ActionPerformed -> taskViewModel.onCheckedChange(task)
}
}
}
Scaffold(
scaffoldState = scaffoldState,
topBar = { HomeTopBar() }
) {
val list by remember(taskViewModel) { taskViewModel.taskList }.collectAsState()
LazyColumn {
items(
items = list,
key = { it.id },
itemContent = { task ->
ListItem(
task = task,
onCheckedChange = { task ->
taskViewModel.onCheckedChange(task)
onShowSnackbar(task)
}
)
}
)
}
}
}
@Composable
private fun ListItem(task: Task, onCheckedChange: (Task) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = task.isCompleted,
onCheckedChange = { onCheckedChange(task) }
)
Spacer(Modifier.width(8.dp))
Text(
text = task.title,
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
}
@Composable
private fun HomeTopBar() {
TopAppBar {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
style = MaterialTheme.typography.h5,
text = "My tasks"
)
}
}
}
data class Task(val id: Long, val title: String, var isCompleted: Boolean)
class ListViewModel : ViewModel() {
private val list = mutableListOf(
Task(1L, "Buy milk", false),
Task(2L, "Watch 'Call Me By Your Name'", false),
Task(3L, "Listen 'Local Natives'", false),
Task(4L, "Study about 'fakes instead of mocks'", false),
Task(5L, "Congratulate Rafael", false),
Task(6L, "Watch Kotlin YouTube Channel", false)
)
private val _taskList: MutableStateFlow<List<Task>> = MutableStateFlow(list)
val taskList: StateFlow<List<Task>>
get() = _taskList.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), listOf())
fun onCheckedChange(task: Task) {
list.find { it.id == task.id }?.isCompleted = task.isCompleted.not()
_taskList.value = list.filter { it.isCompleted.not() }
}
}
От переводчика: комментарии и правки приветствуются.
===========
Источник:
habr.com
===========
===========
Автор оригинала: Igor Escodro
===========Похожие новости:
- [Java, Kotlin] Реактивный масштабируемый чат на Kotlin + Spring + WebSockets
- [Разработка мобильных приложений, Разработка под Android, Разработка под e-commerce, Управление продуктом] История одного личного кабинета, который помог нам сделать 15 000 курьеров и сборщиков немного счастливее
- [Kotlin, Карьера в IT-индустрии, Конференции, Kubernetes] 22 апреля — новый QIWI Server Party
- [Программирование, Разработка мобильных приложений, Учебный процесс в IT, Карьера в IT-индустрии] Апрельский дайджест: приглашаем на онлайн-практикумы и митапы
- [Разработка мобильных приложений, Смартфоны, Игры и игровые приставки] Sony планирует портировать игры PlayStation на смартфоны
- [PHP, Разработка под iOS, API, Dart, Flutter] Уродливый API
- [Программирование, TDD, Разработка под Android, Kotlin] Пишем unit тесты так, чтобы не было мучительно больно
- [JavaScript, Разработка под iOS, Разработка мобильных приложений, Разработка под Android, ERP-системы] Cordova. Опыт Enterprise-проекта
- [IT-инфраструктура, Гаджеты, История IT, IT-компании] Уроки Symbian OS — фиаско топ менеджеров, колосс на глиняных ногах, или неотвратимость бытия?
- [Программирование, Java, Разработка под Android, Rust] Rust — теперь и на платформе Android (перевод)
Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_dizajn_mobilnyh_prilozhenij (Дизайн мобильных приложений), #_android, #_kotlin, #_jetpack_compose, #_viewmodel, #_snackbar, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_pod_android (
Разработка под Android
), #_kotlin, #_dizajn_mobilnyh_prilozhenij (
Дизайн мобильных приложений
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:28
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Пользовательский опыт (UX — User Experience) - это то, как пользователи воспринимают продукт и какие впечатления получают от взаимодействия с ним.В процессе разработки мобильного приложения важно делать акцент на пользовательском опыте в ситуациях, когда от пользователя требуется подтверждение каких-либо действий (или, наоборот, отмена уже совершённого действия). Если приложение будет выводить AlertDialog по поводу и без, пользователю это вряд ли понравится.В библиотеке компонентов материального дизайна есть Snackbar — виджет, который используется для отображения сообщений в нижней части приложения.Чем хорош снэкбар:
@Composable
fun SnackbarSample() { val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() val modifier = Modifier Box(modifier.fillMaxSize()) { Button(onClick = { coroutineScope.launch { snackbarHostState.showSnackbar(message = "This is a Snackbar") } }) { Text(text = "Click me!") } SnackbarHost( hostState = snackbarHostState, modifier = Modifier.align(Alignment.BottomCenter) ) } } @Composable
fun HomeList(taskViewModel: ListViewModel = viewModel()) { Scaffold { val list by remember(taskViewModel) { taskViewModel.taskList }.collectAsState() LazyColumn { items( items = list, itemContent = { task -> ListItem( task = task, onCheckedChange = taskViewModel::onCheckedChange ) } ) } } } @Composable private fun ListItem(task: Task, onCheckedChange: (Task) -> Unit) { Row( modifier = Modifier .fillMaxWidth() .height(64.dp) .padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { Checkbox(checked = task.isCompleted, onCheckedChange = { onCheckedChange(task) }) Spacer(Modifier.width(8.dp)) Text(text = task.title) } } Наше приложение должно реализовывать следующее бизнес-правило: в списке отображаются только незавершённые задачи. После нажатия на чекбокс, задача должна сразу же удаляться из списка. За реализацию этой логики отвечает ViewModel.А теперь давайте реализуем функционал, который позволит пользователю отменять действие. Первым делом создадим лямбда-функцию, которая будет вызываться вместе со снэкбаром, когда пользователь нажимает на чекбокс. @Composable
fun HomeList(taskViewModel: ListViewModel = viewModel()) { val coroutineScope = rememberCoroutineScope() val scaffoldState = rememberScaffoldState() val onShowSnackbar: (Task) -> Unit = { task -> coroutineScope.launch { val snackbarResult = scaffoldState.snackbarHostState.showSnackbar( message = "${task.title} completed", actionLabel = "Undo" ) when (snackbarResult) { SnackbarResult.Dismissed -> Timber.d("Snackbar dismissed") SnackbarResult.ActionPerformed -> taskViewModel.onCheckedChange(task) } } } ... }
data class Task(val id: Long, val title: String, var isCompleted: Boolean)
class ListViewModel : ViewModel() { private val list = mutableListOf( Task(1L, "Buy milk", false), Task(2L, "Watch 'Call Me By Your Name'", false), Task(3L, "Listen 'Local Natives'", false), Task(4L, "Study about 'fakes instead of mocks'", false), Task(5L, "Congratulate Rafael", false), Task(6L, "Watch Kotlin YouTube Channel", false) ) private val _taskList: MutableStateFlow<List<Task>> = MutableStateFlow(list) val taskList: StateFlow<List<Task>> get() = _taskList.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), listOf()) fun onCheckedChange(task: Task) { list.find { it.id == task.id }?.isCompleted = task.isCompleted.not() _taskList.value = list.filter { it.isCompleted.not() } } } Scaffold(scaffoldState = scaffoldState) {
... ListItem( task = task, onCheckedChange = { task -> taskViewModel.onCheckedChange(task) onShowSnackbar(task) } ) } И ещё кое-чтоВ процессе разработки я столкнулся с тем, что после нажатия на чекбокс снэкбар появлялся и тут же исчезал. Решить её мне помог Адам Пауэлл на Kotlinlang в Slack.Проблема заключалась в следующем: снэкбар удалялся из очереди, когда вызов showSnackbar отменялся. Изначально я объявлял rememberCoroutineScope в compose-функции ListItem, которая формировала элементы списка задач. Проблему решил перенос её объявления выше Scaffold.Что дальше?Полный исходный код для это статьи доступен в этом gist-репозитории (и в конце статьи). Не пинайте сильно за реализацию ViewModel. Она примитивна, да, и она здесь лишь для имитации "живых данных". В реальном приложении данные будут поступать, например, из Flow, связанного с Room.Для своего приложения реализацию Undo в снэкбаре я добавил в этом пул-реквесте. Если есть желание, посмотрите.Надеюсь, эта статья поможет вам создавать свои реализации отмены действий в снэкбарах. Спасибо за внимание, а Адаму Пауэллу за помощь с корутинами.Полный исходный код @Composable
fun HomeList(taskViewModel: ListViewModel = viewModel()) { val coroutineScope = rememberCoroutineScope() val scaffoldState = rememberScaffoldState() val onShowSnackbar: (Task) -> Unit = { task -> coroutineScope.launch { val snackbarResult = scaffoldState.snackbarHostState.showSnackbar( message = "${task.title} completed", actionLabel = "Undo" ) when (snackbarResult) { SnackbarResult.Dismissed -> Timber.d("Snackbar dismissed") SnackbarResult.ActionPerformed -> taskViewModel.onCheckedChange(task) } } } Scaffold( scaffoldState = scaffoldState, topBar = { HomeTopBar() } ) { val list by remember(taskViewModel) { taskViewModel.taskList }.collectAsState() LazyColumn { items( items = list, key = { it.id }, itemContent = { task -> ListItem( task = task, onCheckedChange = { task -> taskViewModel.onCheckedChange(task) onShowSnackbar(task) } ) } ) } } } @Composable private fun ListItem(task: Task, onCheckedChange: (Task) -> Unit) { Row( modifier = Modifier .fillMaxWidth() .height(64.dp) .padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { Checkbox( checked = task.isCompleted, onCheckedChange = { onCheckedChange(task) } ) Spacer(Modifier.width(8.dp)) Text( text = task.title, style = MaterialTheme.typography.body1, overflow = TextOverflow.Ellipsis, maxLines = 1 ) } } @Composable private fun HomeTopBar() { TopAppBar { Box(modifier = Modifier.fillMaxSize()) { Text( modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.h5, text = "My tasks" ) } } } data class Task(val id: Long, val title: String, var isCompleted: Boolean) class ListViewModel : ViewModel() { private val list = mutableListOf( Task(1L, "Buy milk", false), Task(2L, "Watch 'Call Me By Your Name'", false), Task(3L, "Listen 'Local Natives'", false), Task(4L, "Study about 'fakes instead of mocks'", false), Task(5L, "Congratulate Rafael", false), Task(6L, "Watch Kotlin YouTube Channel", false) ) private val _taskList: MutableStateFlow<List<Task>> = MutableStateFlow(list) val taskList: StateFlow<List<Task>> get() = _taskList.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), listOf()) fun onCheckedChange(task: Task) { list.find { it.id == task.id }?.isCompleted = task.isCompleted.not() _taskList.value = list.filter { it.isCompleted.not() } } } От переводчика: комментарии и правки приветствуются.
=========== Источник: habr.com =========== =========== Автор оригинала: Igor Escodro ===========Похожие новости:
Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ), #_kotlin, #_dizajn_mobilnyh_prilozhenij ( Дизайн мобильных приложений ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:28
Часовой пояс: UTC + 5