Разработка
Полное руководство по написанию чистого кода Jetpack Compose
Следуя этим рекомендациям, разработчики смогут создавать модульные, поддерживаемые и производительные пользовательские интерфейсы, в полной мере использующие возможности декларативной природы Compose.
Это руководство призвано помочь разработчикам освоить лучшие практики Jetpack Compose, обеспечив правильное наименование, структуру и управление композитными функциями.
В нем рассматриваются такие ключевые принципы, как правильное использование модификаторов, важность компонентов без состояния и контролируемых компонентов, а также эффективная обработка состояния путем его поднятия.
Следуя этим рекомендациям, разработчики смогут создавать модульные, поддерживаемые и производительные пользовательские интерфейсы, в полной мере использующие возможности декларативной природы Compose.
Давайте рассмотрим эти рекомендации и практики, чтобы создавать оптимизированные, хорошо структурированные приложения на Jetpack Compose, которые выдержат испытание временем.
Именуйте @Composable функции
Функции Composable, которые обычно возвращают единицы, должны начинаться с заглавной буквы (PascalCase).
// ✅ Do // This function is a descriptive PascalCased noun as a visual UI element @Composable fun AppTOS(tos: String) {...} // ✅ Do // This function is a descriptive PascalCased noun as a non-visual element // with presence in the composition @Composable 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 👍 @Composable fun softKeyboardVisibilityAsState(): State<Boolean> {...} // ❌ Don't // This function is a noun but is not PascalCased! @Composable fun appTOS(tos: String) {...} // ❌ Don't // This composable returns a state which represents soft keyboard is open or not @Composable fun SoftKeyboardVisibilityAsState(): State<Boolean> {...} // ❌ Don't // This function is PascalCased but is not a noun! @Composable fun FancyRandering(text: String, onClick: () -> Unit) {...} // ❌ Don't // This function is neither PascalCased nor a noun! @Composable fun drawProfileImage(image: ImageAsset) {...}
Упорядочивайте @Composable параметры
Порядок параметров в компоненте может быть задан двумя различными способами:
1. Официальный способ, рекомендуемый Androi
- Необходимые параметры
- `modifier: Modifier = Modifier.`
- Опциональные параметры
- (необязательные) дополнительные @Composable лямбда
2. Последовательный, но достаточно эффективный подход
- `modifier: Modifier = Modifier.`
- Входные данные
- Параметры, связанные с пользовательским интерфейсом (цвет фона, стиль, цвет, форма и т.д.)
- Колбек лямбда-функций
- @Composable содержимое лямбда
// The Official Android recommended way @Composable 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: @Composable RowScope.() -> Unit ) { ... } // A consistent yet modestly effective approach @Composable 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: @Composable RowScope.() -> Unit ) { ... }
Почему второй способ предпочтительнее?
- Он предполагает, что параметры организованы в виде групп, что облегчает поиск и модификацию компонентов.
- Нам не нужно задумываться о том, являются ли параметры обязательными или необязательными. Нам просто нужно добавить их в соответствующую группу. Например, если вы хотите добавить BorderStroke в вышеприведенный пример, просто найдите раздел, где перечислены параметры, связанные с пользовательским интерфейсом, и добавьте его туда.
- Упорядочивание всех композабл параметров будет иметь схожую структуру.
Выдавайте содержимое ИЛИ возвращайте значение
- @Composable функции должны либо выдавать содержимое в композицию, либо возвращать значение, но не то и другое.
- Если компонуемый объект должен предлагать дополнительные управляющие поверхности своему вызывающему объекту, вызывающий объект должен предоставить эти управляющие поверхности или обратные вызовы в качестве параметров для composable функции.
// ✅ Do // Emits only the content @Composable 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 @Composable fun softKeyboardVisibilityAsState(): State<Boolean> {...} // ❌ Don't // Emits content as well as returns a value of text @Composable fun FirstName( modifier: Modifier = Modifier, ): String { var text by remember { mutableStateOf("") } TextField( modifier = modifier, value = text, onValueChange = { text = it } ) return text }
Не генерируйте несколько частей контента
- Композитная функция должна выдавать либо 0, либо 1 фрагмент макета, но не более одного.
- Композитная функция должна быть целостной и не зависеть от того, из какой функции она вызывается.
- Эта практика гарантирует, что composable будет вести себя так, как определено, для всех, кто ее вызывает.
// ✅ 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. @Composable 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. @Composable private fun InnerContent() { Text(...) Image(...) Button(...) }
Composable должен принимать и уважать модификатор
- Composable должен принимать параметр типа Modifier. Этот параметр должен иметь имя «modifier» и должен быть первым параметром композита. Композабл не должен принимать несколько параметров Modifier.
- Composable должен предоставлять свой параметр Modifier ноде Compose UI, который он генерирует, передавая его корневому вызываемому composable. Если Composable непосредственно генерирует узел ноду Compose UI, модификатор должен быть предоставлен этому узлу.
// ✅ Do @Composable 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.
// ✅ Do @Composable 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 @Composable 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 и будет передавать состояние другим композитам.
// ✅ Do // Check how we have just passed required data to the component and used lamba for call backs @Composable 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 @Composable 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 @Composable fun SocialConfigurationRoute( modifier: Modifier = Modifier, viewModel: SocialConfigurationViewModel = hitlViewModel(), ) { SocialConfigurationScreen( modifier = modifier, viewmodel = viewModel ) } // Component of social configuration screen @Composable private fun SocialConfigurationScreen( modifier: Modifier = Modifier, viewModel: ViewModel, // Do not pass viewModel object directly ){ ... } // ❌ Don't @Composable 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.
// ✅ Do // Here we are using padding provided by Scaffold when calling FirstName composable. @Composable fun SocialScreen( modifier: Modifier = Modifier ) { Scaffold( modifier = modifier ) { innerPadding -> FirstName( modifier = Modifier .padding(innerPadding), text = "Hello" ) } } @Composable private fun FirstName( modifier: Modifier = Modifier, text: String ) {...} // ❌ Don't // Here we have not used padding provided by Scaffold and instead passed padding separately. @Composable fun SocialScreen( modifier: Modifier = Modifier ) { Scaffold( modifier = modifier ) { innerPadding -> FirstName( modifier = Modifier .padding(10.dp), text = "Hello" ) } } @Composable private fun FirstName( modifier: Modifier = Modifier, text: String ) {...}
Избегайте добавления некоторых модификаций непосредственно в корневой компонент композита
- Избегайте применения следующих модификаций к корневому компоненту composable, поскольку вызывающая сторона должна управлять этими модификаторами для обеспечения повторного использования и гибкости. Такая практика позволяет не ограничивать использование composable конкретными случаями применения, позволяя внешнему коду контролировать его внешний вид и поведение:
- padding()
- fillMaxWidth()
- fillMaxHeight()
- fillMaxSize()
- width()
- height()
- Избегайте любых других модификаторов, которые управляют компоновкой компонента изнутри.
- Такой подход позволяет вызывающей стороне предоставлять свои модификаторы, гарантируя, что компонент может быть адаптирован к различным сценариям.
// ✅ Do // We are passing padding from the caller side SocialScreenContent( modifier = Modifier .padding(10.dp) ) @Composable 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() @Composable 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 для создания надежных, оптимизированных пользовательских интерфейсов.