Разработка
Полное руководство по написанию чистого кода Jetpack Compose
Следуя этим рекомендациям, разработчики смогут создавать модульные, поддерживаемые и производительные пользовательские интерфейсы, в полной мере использующие возможности декларативной природы Compose.
Это руководство призвано помочь разработчикам освоить лучшие практики Jetpack Compose, обеспечив правильное наименование, структуру и управление композитными функциями.
В нем рассматриваются такие ключевые принципы, как правильное использование модификаторов, важность компонентов без состояния и контролируемых компонентов, а также эффективная обработка состояния путем его поднятия.
Следуя этим рекомендациям, разработчики смогут создавать модульные, поддерживаемые и производительные пользовательские интерфейсы, в полной мере использующие возможности декларативной природы Compose.
Давайте рассмотрим эти рекомендации и практики, чтобы создавать оптимизированные, хорошо структурированные приложения на Jetpack Compose, которые выдержат испытание временем.
Именуйте @Composable функции
Функции Composable, которые обычно возвращают единицы, должны начинаться с заглавной буквы (PascalCase).
// ✅ Do
// This function is a descriptive PascalCased noun as a visual UI element
fun AppTOS(tos: String) {...}
// ✅ Do
// This function is a descriptive PascalCased noun as a non-visual element
// with presence in the composition
fun BackButtonHandler(onBackPressed: () -> Unit) {...}
// ✅ Do
// This composable returns a state which represents soft keyboard is open or not.
// camelCase is allowed when composable returns a value 👍
fun softKeyboardVisibilityAsState(): State<Boolean> {...}
// ❌ Don't
// This function is a noun but is not PascalCased!
fun appTOS(tos: String) {...}
// ❌ Don't
// This composable returns a state which represents soft keyboard is open or not
fun SoftKeyboardVisibilityAsState(): State<Boolean> {...}
// ❌ Don't
// This function is PascalCased but is not a noun!
fun FancyRandering(text: String, onClick: () -> Unit) {...}
// ❌ Don't
// This function is neither PascalCased nor a noun!
fun drawProfileImage(image: ImageAsset) {...}
Упорядочивайте @Composable параметры
Порядок параметров в компоненте может быть задан двумя различными способами:
1. Официальный способ, рекомендуемый Androi
- Необходимые параметры
- `modifier: Modifier = Modifier.`
- Опциональные параметры
- (необязательные) дополнительные @Composable лямбда
2. Последовательный, но достаточно эффективный подход
- `modifier: Modifier = Modifier.`
- Входные данные
- Параметры, связанные с пользовательским интерфейсом (цвет фона, стиль, цвет, форма и т.д.)
- Колбек лямбда-функций
- @Composable содержимое лямбда
xxxxxxxxxx
// The Official Android recommended way
fun BaseButton(
// Required parameter
enabled: Boolean,
onClick: () -> Unit,
// Modifier, first optional parameter
modifier: Modifier = Modifier,
// Optional parameters
backgroundColor: Color = MaterialTheme.colorScheme.inputBG,
shape: Shape = MaterialTheme.shapes.button,
contentPadding: PaddingValues = PaddingValues(),
// (optional) trailing @Composable lambda
content: RowScope.() -> Unit
) { ... }
•
•
// A consistent yet modestly effective approach
fun Button(
// Modifier parameter
modifier: Modifier = Modifier,
// Input data
text: String, // Could be models, primitive data as well
// UI-related parameters
textColor: Color = MaterialTheme.colorScheme.blue
textSiz: Dp = dimensionResource(id = R.dimen.default_text_size)
backgroundColor: Color = MaterialTheme.colorScheme.inputBG,
contentPadding: PaddingValues = PaddingValues(),
// Call back lambda functions.
onClick: () -> Unit,
// trailing @Composable lambda
content: RowScope.() -> Unit
) { ... }
Почему второй способ предпочтительнее?
- Он предполагает, что параметры организованы в виде групп, что облегчает поиск и модификацию компонентов.
- Нам не нужно задумываться о том, являются ли параметры обязательными или необязательными. Нам просто нужно добавить их в соответствующую группу. Например, если вы хотите добавить BorderStroke в вышеприведенный пример, просто найдите раздел, где перечислены параметры, связанные с пользовательским интерфейсом, и добавьте его туда.
- Упорядочивание всех композабл параметров будет иметь схожую структуру.
Выдавайте содержимое ИЛИ возвращайте значение
- @Composable функции должны либо выдавать содержимое в композицию, либо возвращать значение, но не то и другое.
- Если компонуемый объект должен предлагать дополнительные управляющие поверхности своему вызывающему объекту, вызывающий объект должен предоставить эти управляющие поверхности или обратные вызовы в качестве параметров для composable функции.
xxxxxxxxxx
// ✅ Do
// Emits only the content
fun FirstName(
modifier: Modifier = Modifier,
text: String,
onTextChange: (String) -> Unit,
) {
TextField(
modifier = modifier,
value = text,
onValueChange = onTextChange
)
}
•
// ✅ Do
// Returns only value and follows the naming convention
fun softKeyboardVisibilityAsState(): State<Boolean> {...}
•
•
// ❌ Don't
// Emits content as well as returns a value of text
fun FirstName(
modifier: Modifier = Modifier,
): String {
var text by remember { mutableStateOf("") }
TextField(
modifier = modifier,
value = text,
onValueChange = { text = it }
)
return text
}
Не генерируйте несколько частей контента
- Композитная функция должна выдавать либо 0, либо 1 фрагмент макета, но не более одного.
- Композитная функция должна быть целостной и не зависеть от того, из какой функции она вызывается.
- Эта практика гарантирует, что composable будет вести себя так, как определено, для всех, кто ее вызывает.
xxxxxxxxxx
// ✅ Do
// Here we are emiting only the Column, which is responsible for placing the contents.
fun InnerContent(
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(...)
Image(...)
Button(...)
}
}
•
•
// ❌ Don't
// OuterContent is not responsible for ordering the InnerContent.
fun OuterContent(
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
InnerContent()
}
}
•
// This component is emiting multiple pieces of content and assuming that caller side
// Column is used.
private fun InnerContent() {
Text(...)
Image(...)
Button(...)
}
Composable должен принимать и уважать модификатор
- Composable должен принимать параметр типа Modifier. Этот параметр должен иметь имя «modifier» и должен быть первым параметром композита. Композабл не должен принимать несколько параметров Modifier.
- Composable должен предоставлять свой параметр Modifier ноде Compose UI, который он генерирует, передавая его корневому вызываемому composable. Если Composable непосредственно генерирует узел ноду Compose UI, модификатор должен быть предоставлен этому узлу.
xxxxxxxxxx
// ✅ Do
fun ClickableText(
modifier: Modifier = Modifier,
text: String,
onClick: () -> Unit,
) {
Text(
modifier = modifier
.surface(elevation = 4.dp)
.clickable(onClick)
.padding(horizontal = 32.dp, vertical = 16.dp),
text = text,
)
}
Предпочитайте композиты без состояния и управляемые композиты
- В данном контексте без состояния (stateless) относится к композитам, которые не сохраняют собственного состояния, а принимают внешние параметры состояния, которые принадлежат и предоставляются вызывающей стороной.
- Контролируемый (controlled) относится к идее, что вызывающая сторона имеет полный контроль над состоянием, предоставляемым composable.
xxxxxxxxxx
// ✅ Do
fun Checkbox(
modifier: Modifier = Modifier,
isChecked: Boolean,
onToggle: () -> Unit
) { ... }
•
// Usage: (caller mutates optIn and owns the source of truth)
// Assuming that myState is emitted by viewModel and provided through state hoisting
Checkbox(
isChecked = myState.optIn,
onToggle = { myState.optIn = !myState.optIn }
)
•
•
// ❌ Don't
fun Checkbox(
modifier: Modifier = Modifier,
initialValue: Boolean,
onToggle: (isChecked: Boolean) -> Unit
) {
var isChecked by remember { mutableStateOf(initialValue) }
...
}
•
// Usage: (caller mutates optIn and owns the source of truth)
// Assuming that myState is emitted by viewModel and provided through state hoisting
Checkbox(
initialValue = false,
onToggle = { isChecked ->
// The caller can respond to the isChecked value but cannot update the checked state.
}
)
Состояние должно быть поднято вверх
- Для реализации однонаправленного потока Compose поддерживает паттерн подъема состояния вверх, позволяя большинству ваших композитных функций существовать без состояния
- На практике есть несколько общих моментов, на которые следует обратить внимание:
- Не передавайте View Model (или объекты из DI) вниз.
- Не передавайте вниз экземпляры State<Foo> или MutableState<Bar>.
- Вместо этого передавайте в функцию соответствующие данные и необязательные лямбды для обратных вызовов.
- Не беспокойтесь о длинном списке параметров.
- Не все композиты будут stateless; один композит будет stateful и будет передавать состояние другим композитам.
xxxxxxxxxx
// ✅ Do
// Check how we have just passed required data to the component and used lamba for call backs
fun SocialConfigurationRoute(
modifier: Modifier = Modifier,
viewModel: SocialConfigurationViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val contacts = viewModel.contacts.collectAsLazyPagingItems()
var isRefreshing by remember(Unit) { mutableStateOf(false) }
LaunchedEffect(contacts.loadState.refresh) {
isRefreshing = contacts.loadState.refresh is LoadState.Loading
}
SocialConfigurationScreen(
modifier = modifier,
categories = uistate.categories,
isRefreshing = isRefreshing,
onRefresh = ::onRefresh,
onBackClick = viewModel::onBackClick,
)
}
•
// Component of social configuration screen
private fun SocialConfigurationScreen(
modifier: Modifier = Modifier,
categories: List<String>,
isRefreshing: Boolean,
onRefresh: () -> Unit,
onBackClick: () -> Unit
){ ... }
•
•
// ❌ Don't
// Here viewModel parameter is directly passed to the the component
fun SocialConfigurationRoute(
modifier: Modifier = Modifier,
viewModel: SocialConfigurationViewModel = hitlViewModel(),
) {
SocialConfigurationScreen(
modifier = modifier,
viewmodel = viewModel
)
}
•
// Component of social configuration screen
private fun SocialConfigurationScreen(
modifier: Modifier = Modifier,
viewModel: ViewModel, // Do not pass viewModel object directly
){ ... }
•
•
// ❌ Don't
private fun SocialConfigurationScreen(
modifier: Modifier = Modifier,
isRefreshing: MutableState<Boolean>, // Do not pass MutableState directly
user: Flow<User> // Do not pass Flows directly
){ ... }
Используйте Padding, предоставляемый Scaffold
- Всегда используйте стандартный паддинг, предоставляемую Scaffold.
xxxxxxxxxx
// ✅ Do
// Here we are using padding provided by Scaffold when calling FirstName composable.
fun SocialScreen(
modifier: Modifier = Modifier
) {
Scaffold(
modifier = modifier
) { innerPadding ->
FirstName(
modifier = Modifier
.padding(innerPadding),
text = "Hello"
)
}
}
•
private fun FirstName(
modifier: Modifier = Modifier,
text: String
) {...}
•
•
// ❌ Don't
// Here we have not used padding provided by Scaffold and instead passed padding separately.
fun SocialScreen(
modifier: Modifier = Modifier
) {
Scaffold(
modifier = modifier
) { innerPadding ->
FirstName(
modifier = Modifier
.padding(10.dp),
text = "Hello"
)
}
}
•
private fun FirstName(
modifier: Modifier = Modifier,
text: String
) {...}
Избегайте добавления некоторых модификаций непосредственно в корневой компонент композита
- Избегайте применения следующих модификаций к корневому компоненту composable, поскольку вызывающая сторона должна управлять этими модификаторами для обеспечения повторного использования и гибкости. Такая практика позволяет не ограничивать использование composable конкретными случаями применения, позволяя внешнему коду контролировать его внешний вид и поведение:
- padding()
- fillMaxWidth()
- fillMaxHeight()
- fillMaxSize()
- width()
- height()
- Избегайте любых других модификаторов, которые управляют компоновкой компонента изнутри.
- Такой подход позволяет вызывающей стороне предоставлять свои модификаторы, гарантируя, что компонент может быть адаптирован к различным сценариям.
xxxxxxxxxx
// ✅ Do
// We are passing padding from the caller side
SocialScreenContent(
modifier = Modifier
.padding(10.dp)
)
fun SocialScreenContent(
modifier: Modifier = Modifier,
) {
// We are not applying any padding directly to root component of the composable
Column(
modifier = modifier
) {
Text(text = "")
Text(text = "")
}
}
•
•
// ❌ Don't
// No padding used from the caller side
SocialScreenContent()
fun SocialScreenContent(
modifier: Modifier = Modifier,
) {
// Here we have applied padding directly to the root component of the composable
Column(
modifier = modifier
.padding(10.dp)
) {
Text(text = "")
Text(text = "")
}
}
Заключение
Jetpack Compose расширяет возможности разработки пользовательского интерфейса для Android, но освоение лучших практик его использования крайне важно. Придерживаясь описанных здесь «за» и «против», разработчики смогут создавать чистый, удобный в обслуживании код, избегая распространенных ошибок. Принятие этих принципов также поможет избежать неэффективности, обеспечивая создание масштабируемых приложений, использующих весь потенциал Compose для создания надежных, оптимизированных пользовательских интерфейсов.
-
Программирование3 недели назад
Конец программирования в том виде, в котором мы его знаем
-
Видео и подкасты для разработчиков6 дней назад
Как устроена мобильная архитектура. Интервью с тех. лидером юнита «Mobile Architecture» из AvitoTech
-
Магазины приложений3 недели назад
Магазин игр Aptoide запустился на iOS в Европе
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.8