Хотя миграция с Views на Compose связана исключительно с пользовательским интерфейсом, для безопасной и поэтапной миграции необходимо учесть множество факторов. На этой странице представлены некоторые рекомендации по миграции вашего приложения на основе View на Compose.
Перенос темы вашего приложения
Material Design — рекомендуемая система дизайна для тематизации приложений Android.
Для приложений на основе View доступны три версии Material:
- Material Design 1 с использованием библиотеки AppCompat (т.е.
Theme.AppCompat.*
) - Material Design 2 с использованием библиотеки MDC-Android (т.е.
Theme.MaterialComponents.*
) - Material Design 3 с использованием библиотеки MDC-Android (т.е.
Theme.Material3.*
)
Для приложений Compose доступны две версии Material:
- Material Design 2 с использованием библиотеки Compose Material (например,
androidx.compose.material.MaterialTheme
) - Material Design 3 с использованием библиотеки Compose Material 3 (например,
androidx.compose.material3.MaterialTheme
)
Мы рекомендуем использовать последнюю версию (Material 3), если дизайн-система вашего приложения позволяет это. Руководства по миграции доступны как для Views, так и для Compose:
- Материал 1 к Материалу 2 в Видах
- Материал 2 – Материал 3 в представлениях
- Материал 2 – Материал 3 в Compose
При создании новых экранов в Compose, независимо от используемой версии Material Design, обязательно применяйте MaterialTheme
перед любыми компонуемыми элементами, создающими пользовательский интерфейс из библиотек Compose Material. Компоненты Material ( Button
, Text
и т. д.) зависят от наличия MaterialTheme
, и их поведение без него не определено.
Все примеры Jetpack Compose используют пользовательскую тему Compose, созданную на основе MaterialTheme
.
Дополнительные сведения см. в разделах Системы проектирования в Compose и Перенос тем XML в Compose .
Навигация
Если вы используете компонент Navigation в своем приложении, см. разделы Navigating with Compose — Interoperability и Migrate Jetpack Navigation to Navigation Compose для получения дополнительной информации.
Протестируйте смешанный интерфейс «Создать/Просмотреть»
После переноса частей вашего приложения в Compose тестирование имеет решающее значение, чтобы убедиться, что вы ничего не сломали.
Если действие или фрагмент использует Compose, вам необходимо использовать createAndroidComposeRule
вместо ActivityScenarioRule
. createAndroidComposeRule
интегрирует ActivityScenarioRule
с ComposeTestRule
, что позволяет вам одновременно тестировать код Compose и View.
class MyActivityTest { @Rule @JvmField val composeTestRule = createAndroidComposeRule<MyActivity>() @Test fun testGreeting() { val greeting = InstrumentationRegistry.getInstrumentation() .targetContext.resources.getString(R.string.greeting) composeTestRule.onNodeWithText(greeting).assertIsDisplayed() } }
Подробнее о тестировании см. в разделе Тестирование макета Compose . О взаимодействии с фреймворками тестирования пользовательского интерфейса см. в разделах Взаимодействие с Espresso и Взаимодействие с UiAutomator .
Интеграция Compose с существующей архитектурой приложения
Архитектурные шаблоны однонаправленного потока данных (UDF) без проблем работают с Compose. Если приложение использует другие архитектурные шаблоны, например, Model View Presenter (MVP), мы рекомендуем перенести эту часть пользовательского интерфейса на UDF до или во время внедрения Compose.
Использование ViewModel
в Compose
Если вы используете библиотеку Architecture Components ViewModel
, вы можете получить доступ к ViewModel
из любого компонуемого объекта, вызвав функцию viewModel()
, как описано в Compose и других библиотеках .
При использовании Compose будьте осторожны с использованием одного и того же типа ViewModel
в разных компонуемых объектах, поскольку элементы ViewModel
следуют областям действия жизненного цикла View. Областью действия будет либо хост-активность, либо фрагмент, либо навигационный граф, если используется библиотека Navigation.
Например, если компонуемые элементы размещены в активности, viewModel()
всегда возвращает один и тот же экземпляр, который очищается только после завершения активности. В следующем примере один и тот же пользователь («user1») приветствуется дважды, поскольку один и тот же экземпляр GreetingViewModel
повторно используется во всех компонуемых элементах в хост-активности. Первый созданный экземпляр ViewModel
повторно используется в других компонуемых элементах.
class GreetingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Column { GreetingScreen("user1") GreetingScreen("user2") } } } } } @Composable fun GreetingScreen( userId: String, viewModel: GreetingViewModel = viewModel( factory = GreetingViewModelFactory(userId) ) ) { val messageUser by viewModel.message.observeAsState("") Text(messageUser) } class GreetingViewModel(private val userId: String) : ViewModel() { private val _message = MutableLiveData("Hi $userId") val message: LiveData<String> = _message } class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return GreetingViewModel(userId) as T } }
Поскольку графы навигации также охватывают элементы ViewModel
, компонуемые объекты, являющиеся пунктом назначения в графе навигации, имеют отдельный экземпляр ViewModel
. В этом случае область действия ViewModel
ограничена жизненным циклом пункта назначения и очищается при удалении пункта назначения из стека возвратов. В следующем примере, когда пользователь переходит на экран профиля , создаётся новый экземпляр GreetingViewModel
.
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
Государственный источник истины
При внедрении Compose в одну из частей пользовательского интерфейса может возникнуть необходимость в общих данных для Compose и кода системы View. По возможности мы рекомендуем инкапсулировать это общее состояние в другом классе, соответствующем лучшим практикам UDF, принятым на обеих платформах; например, в ViewModel
, который предоставляет поток общих данных для обновления данных.
Однако это не всегда возможно, если данные, которыми нужно поделиться, изменяемы или тесно связаны с элементом пользовательского интерфейса. В этом случае одна система должна быть источником данных, и эта система должна передавать любые обновления данных другой системе. Как правило, источником данных должен быть элемент, расположенный ближе к корню иерархии пользовательского интерфейса.
Сочинение как источник истины
Используйте компонуемый объект SideEffect
для публикации состояния Compose в коде, не поддерживающем Compose. В этом случае источник истинности хранится в компонуемом объекте, который отправляет обновления состояния.
Например, ваша аналитическая библиотека может позволить вам сегментировать аудиторию пользователей, добавляя пользовательские метаданные (в данном примере — свойства пользователя ) ко всем последующим аналитическим событиям. Чтобы сообщить тип текущего пользователя в аналитическую библиотеку, используйте SideEffect
для обновления его значения.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
Более подробную информацию см. в разделе Побочные эффекты в Compose .
Рассматривайте систему как источник истины
Если система View владеет состоянием и разделяет его с Compose, мы рекомендуем обернуть состояние в объекты mutableStateOf
, чтобы сделать его потокобезопасным для Compose. При использовании этого подхода компонуемые функции упрощаются, поскольку у них больше нет источника истинности, но системе View необходимо обновлять изменяемое состояние и представления, использующие это состояние.
В следующем примере CustomViewGroup
содержит TextView
и ComposeView
с компонуемым внутри TextField
. TextView
должен отображать содержимое, которое пользователь вводит в TextField
.
class CustomViewGroup @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : LinearLayout(context, attrs, defStyle) { // Source of truth in the View system as mutableStateOf // to make it thread-safe for Compose private var text by mutableStateOf("") private val textView: TextView init { orientation = VERTICAL textView = TextView(context) val composeView = ComposeView(context).apply { setContent { MaterialTheme { TextField(value = text, onValueChange = { updateState(it) }) } } } addView(textView) addView(composeView) } // Update both the source of truth and the TextView private fun updateState(newValue: String) { text = newValue textView.text = newValue } }
Миграция общего пользовательского интерфейса
При постепенном переходе на Compose вам может потребоваться использовать общие элементы пользовательского интерфейса как в Compose, так и в системе View. Например, если в вашем приложении есть собственный компонент CallToActionButton
, вам может потребоваться использовать его как на экранах Compose, так и на экранах View.
В Compose общие элементы пользовательского интерфейса становятся компонуемыми, которые можно повторно использовать в приложении, независимо от того, оформлен ли элемент с помощью XML или представляет собой пользовательское представление. Например, можно создать компонуемый элемент CallToActionButton
для компонента Button
призыва к действию.
Чтобы использовать компонуемый элемент на экранах, основанных на View, создайте пользовательскую оболочку представления, расширяющую AbstractComposeView
. В его переопределенном компонуемом элементе Content
разместите созданный вами компонуемый элемент, обёрнутый в тему Compose, как показано в примере ниже:
@Composable fun CallToActionButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Button( colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondary ), onClick = onClick, modifier = modifier, ) { Text(text) } } class CallToActionViewButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var text by mutableStateOf("") var onClick by mutableStateOf({}) @Composable override fun Content() { YourAppTheme { CallToActionButton(text, onClick) } } }
Обратите внимание, что компонуемые параметры становятся изменяемыми переменными внутри пользовательского представления. Это делает пользовательское представление CallToActionViewButton
наполняемым и пригодным для использования, как и традиционное представление. Пример использования View Binding ниже:
class ViewBindingActivity : ComponentActivity() { private lateinit var binding: ActivityExampleBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityExampleBinding.inflate(layoutInflater) setContentView(binding.root) binding.callToAction.apply { text = getString(R.string.greeting) onClick = { /* Do something */ } } } }
Если пользовательский компонент содержит изменяемое состояние, см. раздел Источник истины состояния .
Приоритет разделения состояния из представления
Традиционно View
сохраняет состояние. View
управляет полями, описывающими, что именно должно отображаться, а также как это должно быть отображено. При преобразовании View
в Compose постарайтесь разделить отображаемые данные для достижения однонаправленного потока данных, как описано далее в разделе «Поднятие состояния» .
Например, у View
есть свойство visibility
, которое описывает, является ли оно видимым, невидимым или отсутствует. Это неотъемлемое свойство View
. Хотя другие фрагменты кода могут изменять видимость View
, только само View
знает, какова его текущая видимость. Логика, обеспечивающая видимость View
, может быть подвержена ошибкам и часто привязана к самому View
.
В отличие от этого, Compose позволяет легко отображать совершенно разные компонуемые объекты, используя условную логику в Kotlin:
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
По замыслу CautionIcon
не должен знать или заботиться о том, почему он отображается, и не существует понятия visibility
: он либо есть в композиции, либо нет.
Чётко разделяя управление состоянием и логику представления, вы можете более свободно изменять способ отображения контента, преобразуя состояние в пользовательский интерфейс. Возможность «поднимать» состояние при необходимости также делает компонуемые элементы более удобными для повторного использования, поскольку владение состоянием становится более гибким.
Продвижение инкапсулированных и повторно используемых компонентов
Элементы View
часто имеют определённое представление о том, где они находятся: внутри Activity
, Dialog
, Fragment
или где-то внутри другой иерархии View
. Поскольку они часто формируются из статических файлов макета, общая структура View
, как правило, очень жёсткая. Это приводит к более тесной связанности и затрудняет изменение или повторное использование View
.
Например, пользовательское View
может предполагать наличие дочернего представления определенного типа с определенным идентификатором и изменять его свойства непосредственно в ответ на какое-либо действие. Это тесно связывает эти элементы View
: пользовательское View
может выйти из строя или быть неработоспособным, если не сможет найти дочернее представление, и дочернее представление, скорее всего, не сможет быть повторно использовано без родительского пользовательского View
.
В Compose это не такая уж большая проблема с повторно используемыми составными объектами. Родительские объекты могут легко указывать состояние и обратные вызовы, поэтому вы можете создавать повторно используемые составные объекты, не зная точное место их использования.
@Composable fun AScreen() { var isEnabled by rememberSaveable { mutableStateOf(false) } Column { ImageWithEnabledOverlay(isEnabled) ControlPanelWithToggle( isEnabled = isEnabled, onEnabledChanged = { isEnabled = it } ) } }
В приведенном выше примере все три части более инкапсулированы и менее связаны:
ImageWithEnabledOverlay
достаточно знать только текущее состояниеisEnabled
. Ему не нужно знать о существованииControlPanelWithToggle
или даже о том, как им управлять.ControlPanelWithToggle
не знает о существованииImageWithEnabledOverlay
. ОтображениеisEnabled
может быть нулевым, одним или несколькими способами, иControlPanelWithToggle
не придётся менять.Для родительского элемента не имеет значения глубина вложенности
ImageWithEnabledOverlay
илиControlPanelWithToggle
. Эти дочерние элементы могут анимировать изменения, заменять контент или передавать его другим дочерним элементам.
Этот шаблон известен как инверсия управления , подробнее о котором можно прочитать в документации CompositionLocal
.
Обработка изменений размера экрана
Использование различных ресурсов для разных размеров окон — один из основных способов создания адаптивных макетов View
. Хотя квалифицированные ресурсы по-прежнему остаются вариантом для принятия решений о макетах на уровне экрана, Compose значительно упрощает изменение макетов полностью в коде с помощью обычной условной логики. Подробнее см. в разделе Использование классов размеров окон .
Кроме того, ознакомьтесь с разделом Поддержка различных размеров дисплеев , чтобы узнать о методах, которые Compose предлагает для создания адаптивных пользовательских интерфейсов.
Вложенная прокрутка с представлениями
Дополнительную информацию о том, как включить взаимодействие вложенной прокрутки между прокручиваемыми элементами View и прокручиваемыми составными элементами, вложенными в обоих направлениях, см. в разделе Взаимодействие вложенной прокрутки .
Написать в RecyclerView
Компонуемые элементы в RecyclerView
стали производительнее, начиная с версии RecyclerView
1.3.0-alpha02. Чтобы увидеть эти преимущества, убедитесь, что у вас установлена версия RecyclerView
не ниже 1.3.0-alpha02.
Взаимодействие WindowInsets
с представлениями
Вам может потребоваться переопределить вставки по умолчанию, если на вашем экране есть код Views и Compose в одной иерархии. В этом случае вам нужно явно указать, какой из них должен использовать вставки, а какой — игнорировать.
Например, если ваш внешний макет — это макет Android View, вам следует использовать вставки в системе View и игнорировать их для Compose. В качестве альтернативы, если ваш внешний макет — компонуемый, вам следует использовать вставки в Compose и соответствующим образом заполнять компонуемые элементы AndroidView
.
По умолчанию каждый ComposeView
использует все вставки на уровне WindowInsetsCompat
. Чтобы изменить это поведение по умолчанию, установите для ComposeView.consumeWindowInsets
значение false
.
Более подробную информацию можно найти в документации WindowInsets
в Compose .
Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Отображать эмодзи
- Material Design 2 в Compose
- Вставки окон в Compose