Хотя миграция с 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