Хотя переход от Views к Compose связан исключительно с пользовательским интерфейсом, существует множество вещей, которые следует учитывать для выполнения безопасной и инкрементной миграции. На этой странице приведены некоторые рекомендации по переносу приложения на основе View в Compose.
Перенос темы вашего приложения
Material Design — рекомендуемая система дизайна для оформления тем приложений Android.
Для приложений на основе представлений доступны три версии 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
).
Мы рекомендуем использовать последнюю версию (Материал 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 с существующей архитектурой вашего приложения
Архитектурные шаблоны однонаправленного потока данных (UDF) без проблем работают с Compose. Если вместо этого приложение использует другие типы архитектурных шаблонов, например Model View Presenter (MVP), мы рекомендуем перенести эту часть пользовательского интерфейса в UDF до или во время внедрения Compose.
Использование ViewModel
в Compose
Если вы используете библиотеку ViewModel
компонентов архитектуры , вы можете получить доступ к ViewModel
из любого компонуемого объекта, вызвав функцию viewModel()
, как описано в Compose и других библиотеках .
При использовании Compose будьте осторожны при использовании одного и того же типа ViewModel
в разных составных объектах, поскольку элементы ViewModel
соответствуют областям жизненного цикла View. Областью действия будет либо активность узла, либо фрагмент, либо граф навигации, если используется библиотека навигации.
Например, если компонуемые объекты размещаются в действии, 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. Если вы используете этот подход, компонуемые функции упрощаются, поскольку у них больше нет источника истины, но системе представлений необходимо обновить изменяемое состояние и представления, которые используют это состояние.
В следующем примере 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 общие элементы пользовательского интерфейса становятся составными элементами, которые можно повторно использовать в приложении, независимо от того, стилизован ли элемент с использованием XML или является пользовательским представлением. Например, вы можете создать составной элемент CallToActionButton
для своего пользовательского компонента Button
призыва к действию.
Чтобы использовать компоновку на экранах на основе представлений, создайте пользовательскую оболочку представления, которая является наследником 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
вместе: пользовательский 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