Хотя миграция с 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 .
Навигация
Если вы используете компонент «Навигация» в своем приложении, см. разделы «Навигация с помощью Compose — взаимодействие» и «Миграция навигации Jetpack в Navigation Compose» для получения дополнительной информации.
Протестируйте свой смешанный интерфейс Compose/Views
После переноса частей вашего приложения в 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 с существующей архитектурой приложения
Шаблоны архитектуры Unidirectional Data Flow (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
ограничен жизненным циклом пункта назначения и очищается, когда пункт назначения удаляется из обратного стека. В следующем примере, когда пользователь переходит на экран Profile , создается новый экземпляр GreetingViewModel
.
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
Государственный источник истины
При внедрении Compose в одной части пользовательского интерфейса может возникнуть необходимость в том, чтобы Compose и системный код View обменивались данными. Когда это возможно, мы рекомендуем вам инкапсулировать это общее состояние в другом классе, который следует лучшим практикам UDF, используемым обеими платформами; например, в ViewModel
, который предоставляет поток общих данных для выпуска обновлений данных.
Однако это не всегда возможно, если данные, которыми нужно поделиться, изменяемы или тесно связаны с элементом пользовательского интерфейса. В этом случае одна система должна быть источником истины, и эта система должна делиться любыми обновлениями данных с другой системой. Как правило, источником истины должен владеть тот элемент, который ближе к корню иерархии пользовательского интерфейса.
Сочинение как источник истины
Используйте SideEffect
composable для публикации состояния Compose в не-Compose коде. В этом случае источник истины хранится в composable, который отправляет обновления состояния.
Например, ваша аналитическая библиотека может позволить вам сегментировать вашу пользовательскую популяцию, прикрепляя пользовательские метаданные ( свойства пользователя в этом примере) ко всем последующим аналитическим событиям. Чтобы сообщить тип текущего пользователя в вашу аналитическую библиотеку, используйте 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 должна обновить изменяемое состояние и Views, которые используют это состояние.
В следующем примере 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-based экранах.
В 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
надуваемым и пригодным для использования, как и традиционное представление. Ниже приведен пример этого с привязкой представления :
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 определенного типа с определенным id, и изменять его свойства напрямую в ответ на какое-то действие. Это тесно связывает эти элементы 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
Вам может потребоваться переопределить вставки по умолчанию, когда на вашем экране есть и Views, и Compose code в одной и той же иерархии. В этом случае вам нужно будет явно указать, какой из них должен потреблять вставки, а какой должен их игнорировать.
Например, если ваш внешний макет — это макет Android View, вы должны использовать вставки в системе View и игнорировать их для Compose. В качестве альтернативы, если ваш внешний макет — это компонуемый, вы должны использовать вставки в Compose и дополнять компонуемые AndroidView
соответствующим образом.
По умолчанию каждый ComposeView
потребляет все вставки на уровне потребления WindowInsetsCompat
. Чтобы изменить это поведение по умолчанию, установите ComposeView.consumeWindowInsets
в false
.
Для получения дополнительной информации прочтите документацию WindowInsets
в Compose .
Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Отображение эмодзи
- Material Design 2 в Compose
- Вставки окон в Compose