Уже середина 2024 года. Как глухой независимый разработчик, я часто время от времени размышляю о себе: какого прогресса я достиг за последние шесть месяцев? Я от всего сердца посвящаю себя Jetpack Compose и Material Design При изучении и практике 3 (М3) используется это да Jetpack Compose、M3 и Kotlin язык реализованNimReplyApp процесс разработки. Независимо от того, являетесь ли вы новичком или опытным разработчиком, я считаю, что эта статья может вдохновить вас.
NimReplyApp да Кейс-проект, имитирующий почтовое приложение.,Пользователи могут просматривать электронную почту, просматривать детали и отправлять ответы.,Это очень распространено в повседневной работе и жизни.
Почему стоит выбрать Jetpack Compose и Material Design 3? Это связано с тем, что это привело к реформе модели разработки, эффективность разработки очень высока, а пользовательский интерфейс Код прост для понимания и поддержки, он может реализовать сложную анимацию и управление состоянием, устраняя множество традиционных действий. UI Ручные операции в разработке.
dependencies {
implementation "androidx.compose.ui:ui:1.5.0"
implementation "androidx.compose.material3:material3:1.2.0"
implementation "androidx.compose.ui:ui-tooling-preview:1.5.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
implementation "androidx.activity:activity-compose:1.7.2"
implementation "androidx.navigation:navigation-compose:2.5.3"
}
ReplyApp MVVM (Модель-Представление-ViewModel). мы понимаем Логика пользовательского интерфейса, обработка данных и состояние интерфейса управляются отдельно, что делает код чистым и удобным в сопровождении.
Основная структура каталогов проекта выглядит следующим образом:
Объект, представляющий учетную запись, которая может принадлежать пользователю, а один пользователь может иметь несколько учетных записей.
data class Account(
val id: Long,
val uid: Long,
val firstName: String,
val lastName: String,
val email: String,
val altEmail: String,
@DrawableRes val avatar: Int,
var isCurrentAccount: Boolean = false
) {
val fullName: String = "$firstName $lastName"
}
Класс данных электронной почты используется для представления электронных писем.
data class Email(
val id: Long,
val sender: Account,
val recipients: List<Account> = emptyList(),
val subject: String,
val body: String,
val attachments: List<EmailAttachment> = emptyList(),
var isImportant: Boolean = false,
var isStarred: Boolean = false,
var mailbox: MailboxType = MailboxType.INBOX,
val createdAt: String,
val threads: List<Email> = emptyList()
)
В этой демонстрации я разработал несколько ключевых UI Компоненты, включая панель поиска, список рассылки, сведения о почте и т. д., и переданы Preview Для повышения эффективности разработки реализована функция предварительного просмотра в реальном времени. Далее основное внимание уделяется ReplyDockedSearchBar
、ReplyEmailListItem
а также ReplyEmailThreadItem
и другие основные компоненты.
ReplyProfileImage
——Отображение аватара пользователякомпонентыReplyProfileImage
Это простой компонент отображения аватара, обычно используемый для отображения аватара отправителя или получателя. Этот компонент использует Image
компоненты, комбинированные Modifier
Добейтесь эффекта круглого аватара.
@Composable
fun ReplyProfileImage(
drawableResource: Int,
description: String,
modifier: Modifier = Modifier
) {
Image(
modifier = modifier
.size(40.dp)
.clip(CircleShape),
painter = painterResource(id = drawableResource),
contentDescription = description
)
}
Image
компоненты:Используется для отображения пользователейиз Картинка аватара,проходить painterResource
Загрузите указанный файл ресурсов.Modifier
и CircleShape
:проходить Modifier.clip(CircleShape)
,Аватар обрезается до кругового эффекта,Используемый размер изображения size(40.dp)
Возьмите под свой контроль.Modifier
:этоткомпоненты Прием внешних входящихиз modifier
,При использовании компоненты могут быть расширены и настроены в соответствии с различными требованиями компоновки.@Preview(showBackground = true)
@Composable
fun PreviewReplyProfileImage() {
ReplyProfileImage(
drawableResource = R.drawable.nim,
description = "NimDrawable"
)
}
ReplyDockedSearchBar
——С функцией поискаиз Панель поиска по электронной почтеReplyDockedSearchBar
Это верхний компонент панели поиска, который поддерживает поиск в реальном времени. Введите здесь ключевые слова, чтобы отфильтровать соответствующие электронные письма. Этот компонент использует Jetpack Compose Новости Управление статусами LazyColumn
Отображение результатов поиска.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyDockedSearchBar(
emails: List<Email>,
onSearchItemSelected: (Email) -> Unit,
modifier: Modifier = Modifier
) {
var query by remember { mutableStateOf("") }
var expanded by remember { mutableStateOf(false) }
val searchResults = remember { mutableStateListOf<Email>() }
LaunchedEffect(query) {
searchResults.clear()
if (query.isNotEmpty()) {
searchResults.addAll(
emails.filter {
it.subject.startsWith(query, ignoreCase = true) ||
it.sender.fullName.startsWith(query, ignoreCase = true)
}
)
}
}
DockedSearchBar(
inputField = {
SearchBarDefaults.InputField(
query = query,
onQueryChange = { query = it },
onSearch = { expanded = false },
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text(text = «Поиск электронной почты...») }
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
content = {
if (searchResults.isNotEmpty()) {
LazyColumn {
items(searchResults) { email ->
ListItem(
headlineContent = { Text(email.subject) },
supportingContent = { Text(email.sender.fullName) },
leadingContent = { ReplyProfileImage(drawableResource = email.sender.avatar, description = email.sender.fullName) }
)
}
}
} else {
Text(text = if (query.isNotEmpty()) «Результатов не найдено» else «Начни вводить поиск»)
}
}
)
}
LazyColumn
,Он используется для отображения отфильтрованного списка рассылки.LaunchedEffect
для мониторинга query
(Ключевые слова для поиска)изизменять,Динамическое обновление результатов поиска на основе введенных данных.DockedSearchBar
да M3 компоненты панели поиска, используйте его для реализации функций поиска с помощью настраиваемых InputField
Обработка входных данных поиска.@Preview(showBackground = true)
@Composable
fun PreviewReplyDockedSearchBar() {
ReplyDockedSearchBar(emails = sampleEmailList, onSearchItemSelected = {})
}
EmailDetailAppBar
——Используется для отображения сведений об электронной почтеизверхняя панельEmailDetailAppBar
да Верхняя панель навигации страницы сведений об электронном письме, обычно используемая для отображения заголовка и номера ответа на электронное письмо. также операции возврата и другие функции. Использовал M3 предоставил TopAppBar
компонентов, настраивая стиль и содержание.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EmailDetailAppBar(
email: Email,
isFullScreen: Boolean,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit
) {
TopAppBar(
modifier = modifier,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.inverseOnSurface
),
title = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = if (isFullScreen) Alignment.CenterHorizontally else Alignment.Start
) {
Text(
text = email.subject,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
modifier = Modifier.padding(top = 4.dp),
text = "${email.threads.size} ${stringResource(id = R.string.messages)}",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline
)
}
},
navigationIcon = {
if (isFullScreen) {
FilledIconButton(
onClick = onBackPressed,
modifier = Modifier.padding(8.dp),
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.back_button),
modifier = Modifier.size(14.dp)
)
}
}
},
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(id = R.string.more_options_button),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
)
}
TopAppBar
:этоткомпонентыда M3 компоненты Компоненты навигации верхней панели, используемые для отображения заголовка и значка навигации приложения и Кнопка действия,Отрегулируйте выравнивание заголовка в зависимости от полноэкранного режима: при отображении в полноэкранном режиме.,Выровнять заголовок по центру,Выровнено по левому краю, если не в полноэкранном режиме.subject
,Ниже показана цепочка сообщения (количество ответов),проходить Column
Комбинируйте заголовки и подзаголовки.onBackPressed
Обратный вызов для уведомления родителя о необходимости выполнения операции возврата.MoreVert
Значок (кнопка дополнительных параметров), используемый для расширения последующих функций (таких как сбор, обмен и т. д.).@Preview(showBackground = true)
@Composable
fun PreviewEmailDetailAppBar() {
EmailDetailAppBar(
email = sampleEmail,
isFullScreen = true,
onBackPressed = {}
)
}
ReplyEmailListItem
——Используется для отображения элементов списка рассылки.ReplyEmailListItem
Компонент представляет собой компонент отображения элемента списка каждого электронного письма, посредством Card
Упаковка: пользователи могут перейти на страницу сведений об электронной почте, щелкнув элемент списка.
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ReplyEmailListItem(
email: Email,
navigateToDetail: (Long) -> Unit,
toggleSelection: (Long) -> Unit,
isOpened: Boolean = false,
isSelected: Boolean = false
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.combinedClickable(
onClick = { navigateToDetail(email.id) },
onLongClick = { toggleSelection(email.id) }
),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer
else if (isOpened) MaterialTheme.colorScheme.secondaryContainer
else MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = email.subject, style = MaterialTheme.typography.titleMedium)
Text(text = email.sender.fullName, style = MaterialTheme.typography.bodyMedium)
}
}
}
Card
да Jetpack Compose детские Очень практично UI Компоненты: каждое электронное письмо оформлено в виде затененной карточки.combinedClickable
Используется для обработки событий щелчка и длительного нажатия.,Нажмите, чтобы перейти на страницу подробностей,Длительное нажатие, чтобы выбрать адрес электронной почты.@Preview(showBackground = true)
@Composable
fun PreviewReplyEmailListItem() {
ReplyEmailListItem(
email = sampleEmail,
navigateToDetail = {},
toggleSelection = {},
isOpened = false,
isSelected = false
)
}
SelectedProfileImage
——выбранный штатиз Отображение аватараКогда вам нужно выполнить специальную обработку выбранного состояния, например, придать выбранному состоянию другой цвет фона или отобразить Check
значок указывает, что он был выбран。SelectedProfileImage
Нужна цель.
@Composable
fun SelectedProfileImage(modifier: Modifier = Modifier) {
Box(
modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
) {
Icon(
Icons.Default.Check,
contentDescription = null,
modifier = Modifier
.size(24.dp)
.align(Alignment.Center),
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
Box
:для аватараизфони Check
значки сгруппированы вместе。Box
да Простой контейнер, который может выравнивать перекрывающиеся пары контента. использовать Box
Круглый фон аватара Check
Значки наложены друг на друга.size
и clip(CircleShape)
:Сначала установите аватаризразмердля 40.dp
,проходить clip(CircleShape)
Обрезатьдлякруглый。CircleShape
да Compose Предопределенные формы для создания круговых видов.background(MaterialTheme.colorScheme.primary)
:Установить цвет фонадлятемаизосновной цвет,Указывает, что оно выбрано.Icon
:в аватареиз Покажи один в центре Check
Значок, использование цвета значка MaterialTheme.colorScheme.onPrimary
,Контрастный цвет фона,Очень заметно.@Preview(showBackground = true)
@Composable
fun PreviewSelectedProfileImage() {
SelectedProfileImage()
}
за почтовое отправление(ReplyEmailListItem
)при выборе,Еще нужно использовать SelectedProfileImage
Заменяет аватар пользователя по умолчанию, указывая, что он был выбран.
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ReplyEmailListItem(
email: Email,
navigateToDetail: (Long) -> Unit,
toggleSelection: (Long) -> Unit,
modifier: Modifier = Modifier,
isOpened: Boolean = false,
isSelected: Boolean = false,
) {
Card(
modifier = modifier
.padding(horizontal = 16.dp, vertical = 4.dp)
.semantics { selected = isSelected }
.combinedClickable(
onClick = { navigateToDetail(email.id) },
onLongClick = { toggleSelection(email.id) }
),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer
else if (isOpened) MaterialTheme.colorScheme.secondaryContainer
else MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
Row(modifier = Modifier.fillMaxWidth()) {
val clickModifier = Modifier.clickable { toggleSelection(email.id) }
if (isSelected) {
SelectedProfileImage(modifier = clickModifier) // Показать выбранный аватар
} else {
ReplyProfileImage(
drawableResource = email.sender.avatar,
description = email.sender.fullName,
modifier = clickModifier
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = email.sender.firstName,
style = MaterialTheme.typography.labelMedium
)
Text(
text = email.createdAt,
style = MaterialTheme.typography.labelMedium,
)
}
IconButton(
onClick = { /*TODO*/ },
modifier = Modifier.clip(CircleShape)
) {
Icon(
imageVector = Icons.Default.StarBorder,
contentDescription = "Favorite",
tint = MaterialTheme.colorScheme.outline
)
}
}
Text(
text = email.subject,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(top = 12.dp, bottom = 8.dp)
)
Text(
text = email.body,
style = MaterialTheme.typography.bodyMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
isSelected
параметр:проходитьэтот Логическое значение для оценкикогдабывшая почтада Оно выбрано?。когда isSelected
для true
когда, показать SelectedProfileImage
,В остальном отображается нормальноиз ReplyProfileImage
。isSelected
или isOpened
Статус меняется динамически.SelectedProfileImage
:проходить Условное суждение,когда isSelected
для true
когда, Показать выбранный аватар статуса.@Preview(showBackground = true)
@Composable
fun PreviewReplyEmailListItemSelected() {
ReplyEmailListItem(
email = sampleEmail,
navigateToDetail = {},
toggleSelection = {},
modifier = Modifier.fillMaxWidth(),
isOpened = true, // Указывает, что электронное письмо было открыто
isSelected = true // Указывает, что адрес электронной почты выбран.
)
}
ReplyEmailThreadItem
——Отображение ветки электронной почтыкомпонентыКогда пользователь просматривает детали электронного письма, что может включать в себя несколько ответов на электронное письмо, необходимо использовать ReplyEmailThreadItem
Компонент отображает каждый ответ.
@Composable
fun ReplyEmailThreadItem(
email: Email,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.padding(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)
) {
Column(modifier = Modifier.padding(20.dp)) {
Row {
ReplyProfileImage(drawableResource = email.sender.avatar, description = email.sender.fullName)
Column(modifier = Modifier.weight(1f).padding(12.dp)) {
Text(text = email.sender.firstName, style = MaterialTheme.typography.labelMedium)
Text(text = «1 день назад», style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline)
}
}
Text(text = email.subject, style = MaterialTheme.typography.bodyMedium)
Text(text = email.body, style = MaterialTheme.typography.bodyLarge)
}
}
}
ReplyEmailThreadItem
Используется для отображения конкретного содержимого электронного письма, подходит для отображения нескольких ответов на электронное письмо. оно проходит Card
и Row
、Column
Компоненты реализуют упорядочение информации.ReplyProfileImage
Используется для отображения информации об аватаре отправителя.@Preview(showBackground = true)
@Composable
fun PreviewReplyEmailThreadItem() {
ReplyEmailThreadItem(email = sampleEmailThread)
}
Продолжение: во второй части будет показано, как реализовать NimReplyAppLogic), так что следите за обновлениями.
Не стесняйтесь задавать любые вопросы, спасибо всем, кто дочитал)