Основой пользовательского интерфейса в Jetpack Compose являются компоненты, которые оформлены в виде функции с аннотацией @Composable. При вызове @Composable-компонента обычно передаются некоторые данные и набор свойств, которые определяют отображение и поведение определенной части пользовательского интерфейса. По сути, функции @Composable преобразуют данные в элементы пользовательского интерфейса. Эти функции не возвращают значения в традиционном смысле функции Kotlin, а вместо этого определяют элементы пользовательского интерфейса для рендеринга.
@Composable-функции могут вызывать другие подобные компоненты для создания иерархии компонентов. При разработке приложений с помощью Compose мы можем как определять свои собственные компоненты, так и использовать встроенные. Собственно типичный пользовательский интерфейс на основе Compose обычно состоит из комбинации встроенных и кастомных @Composable-компонентов.
Встроенные компоненты, которые по умолчанию входят в состав Compose, можно разделить на три категории: Layout, Foundation и Material Design.
В категорию Layout входят компоненты, которые определяют макет приложения, позволяют определить расположение других компонентов на экране. В частности, это следуюшие компоненты:
Box
BoxWithConstraints
Column
ConstraintLayout
Row
Компоненты Foundation — это набор минимальных компонентов, которые обеспечивают базовую функциональность пользовательского интерфейса. Например, это:
BaseTextField
Canvas
Image
LazyColumn
LazyRow
Shape
Text
Компоненты Material Design разработаны так, чтобы они соответствовали рекомендациям визуальному стилю Google Material. Например, это:
AlertDialog
Button
Card
CircularProgressIndicator
DropdownMenu
Checkbox
FloatingActionButton
LinearProgressIndicator
ModalDrawer
RadioButton
Scaffold
Slider
Snackbar
Switch
TextField
TopAppBar
BottomNavigation
Аналогично мы могли бы создать свой компонент на основе имеющихся и использовать его в качестве корневого компонента. Например, изменим код следующим образом:
package com.example.helloapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.sp class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Hello() } } } @Composable fun Hello() { Text(text = "Hello METANIT.COM!", style = TextStyle( fontSize = 28.sp ) ) }
Здесь мы определили свой компонент Hello, внутри которого по сути используется встроенный компонент Text. Чтобы функция Hello рассматривалась как компонент, к ней применяется аннотация @Composable
из пакета androidx.compose.runtime
. Причем функции компонентов естественно можно вынести в отдельные файлы и даже пакеты и оттуда подключать.
И также для наших компонентов можно задать предварительный просмотр в Android Studio, что позволяет уже до запуска увидеть, как будет выглядеть приложение. Так, изменим код MainActivity.kt следующим образом:
package com.example.helloapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Hello() } } } @Composable fun Hello() { Text(text = "Hello METANIT.COM!", style = TextStyle( fontSize = 28.sp ) ) } @Preview(showSystemUi = true) @Composable fun HelloPreview() { Hello() }
Здесь добавлен компонент HelloPreview, который использует компонент Hello. К компоненту HelloPreview применяется аннотация
@Preview из пакета androidx.compose.ui.tooling.preview
. Свойство showSystemUi = true
этой аннотации позволяет увидеть интерфейс приложения в целом в области предпросмотра в Android Studio:
Исходный код аннотации Composable выглядит следующим образом:
@MustBeDocumented @Retention(AnnotationRetention.BINARY) @Target( AnnotationTarget.FUNCTION, AnnotationTarget.TYPE, AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.PROPERTY_GETTER ) annotation class Composable
Здесь мы видим, что к классу аннотации, в совю очередь, применяются три других аннотации:
@MustBeDocumented
: указывает, что аннотация является частью общедоступного API и должна быть включена в создаваемую документацию.
@Retention
: сообщает компилятору, как долго должна сохраняться аннотация. Значение AnnotationRetention.BINARY
позволяет сохранить код в бинарном файле
во время компиляции.
@Target
: описывает контексты, в которых применяется этот тип. @Composable можно применять к типам, параметрам, функциям и свойствам.
Последняя аннотация указывает, что мы можем применять аннотацию @Composable гораздо шире, чем просто к функции верхнего уровня, как в примерах выше. Например:
package com.example.helloapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text import androidx.compose.runtime.Composable class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Message("Hello METANIT.COM!").printMessage() } } } class Message(val messageText:String){ @Composable fun printMessage(){ Text(text = messageText) } }
Здесь аннотация @Composable
применяется к функции printMessage внутри класса Message. Эта функция определяет компонент Text, в который передается значение свойства messageText. То есть функция
printMessage выступает в качестве компонента. И мы можем использовать этот компонент, вызывав данную функцию:
setContent { Message("Hello METANIT.COM!").printMessage() }
Соответственно фактически интерфейс будет определяться компонентом Text, который выведет строку "Hello METANIT.COM!":
Теперь пришло время, чтобы сказать чуть подробнее, как устанавливается, корневой компонент. Это происходит в методе setContent. Сигнатура метода setContent() выглядит следующим образом:
fun ComponentActivity.setContent( parent: CompositionContext? = null, content: @Composable () -> Unit ) { ... }
Как можно видеть, setContent()
является функцией расширения типа ComponentActivity
.
Функции расширения добавляют классу дополнительную функциональность без изменения его исходного кода. Это означает, что метод setContent() можно использовать
для любого ComponentActivity или его подклассов, например AppCompatActivity.
Вызов setContent() устанавливает функцию из параметра content в качестве корневого компонента,
который выступает в качестве контейнера для всех остальных компонентов и в который можно добавить произвольное количество других компонентов.
Обратите внимание, что функция content также аннотируется с помощью аннотации @Composable. Так как аннотация @Target
позволяет применять аннотацию @Composable к параметрам функции.
Конкретно в данном случае аннотация @Composable помечает передаваемую параметру content
функцию как Composable-функцию.
Еще один параметр метода setContent() представляет объект CompositionContext, который предствляет контекст композиции интерфейса и используется для рекомпозиции - обновления пользовательского интерфейса.
Определение метода setContent
демонстрирует один из распространенных способов передачи компонентов в другие компоненты.
Подобная организация очень удобна, поскольку мы можем в функции получить компонент, как-то настроить его, выполнить с ним некоторые действия.
Например, определим следующее приложение:
package com.example.helloapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text import androidx.compose.ui.unit.sp class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Message{ Text(text = "Hello World", fontSize = 28.sp) } } } } @Composable fun Message(content: @Composable () -> Unit){ content()}
Здесь определен компонент Message, который через параметр принимает некоторую функцию-компонент. Внутри компонента Message мы просто вызываем переданную функцию.
Методе setContent вызываем компонент Message. А в функции, которая передается в Message через параметр content, вызываем компонент Text:
Message{ Text(text = "Hello World", fontSize = 28.sp) }