Site icon AppTractor

Полное руководство по написанию чистого кода Jetpack 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

2. Последовательный, но достаточно эффективный подход

// 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
) { ... }

Почему второй способ предпочтительнее?

Выдавайте содержимое ИЛИ возвращайте значение

// ✅ 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
}

Не генерируйте несколько частей контента

// ✅ 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 должен принимать и уважать модификатор

// ✅ 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,
    )
}

Предпочитайте композиты без состояния и управляемые композиты

// ✅ 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.
    }
)

Состояние должно быть поднято вверх

// ✅ 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

// ✅ 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
) {...}

Избегайте добавления некоторых модификаций непосредственно в корневой компонент композита

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

Источник

Exit mobile version