Connect with us

Разработка

Полное руководство по написанию чистого кода 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 для создания надежных, оптимизированных пользовательских интерфейсов.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.

Наши партнеры:

LEGALBET

Мобильные приложения для ставок на спорт
Хорошие новости

Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: