Хотя переход с Views на Compose — это исключительно вопрос пользовательского интерфейса, существует множество факторов, которые необходимо учитывать для безопасной и поэтапной миграции. На этой странице приведены некоторые рекомендации по миграции вашего приложения на основе Views в Compose.
Перенос темы вашего приложения
Material Design — это рекомендуемая система оформления для приложений Android.
Для приложений, использующих View-интерфейсы, доступны три версии Material Design:
- Material Design 1 с использованием библиотеки AppCompat (т.е.
Theme.AppCompat.*) - Material Design 2 с использованием библиотеки MDC-Android (т.е.
Theme.MaterialComponents.*) - Material Design 3 с использованием библиотеки MDC-Android (т.е.
Theme.Material3.*)
Для приложений Compose доступны две версии Material Design:
- 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, независимо от используемой версии Material Design, убедитесь, что вы применяете MaterialTheme перед любыми компонентами, которые генерируют пользовательский интерфейс из библиотек Material в Compose. Компоненты Material ( Button , Text и т. д.) зависят от наличия MaterialTheme , и их поведение не определено без него.
Во всех примерах Jetpack Compose используется пользовательская тема оформления Compose, созданная на основе MaterialTheme .
Для получения дополнительной информации см. разделы «Системы проектирования в Compose» и «Перенос XML-тем в Compose» .
Навигация
Если вы используете компонент Navigation в своем приложении, см. разделы «Навигация с помощью 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
Если вы используете библиотеку Architecture Components ViewModel , вы можете получить доступ к ViewModel из любого компонуемого объекта, вызвав функцию viewModel() , как описано в разделе «Compose и другие библиотеки» .
При использовании Compose следует проявлять осторожность при работе с одним и тем же типом ViewModel в разных компонуемых объектах, поскольку элементы ViewModel следуют областям видимости жизненного цикла представления. Область видимости будет либо хост-активностью, фрагментом, либо графом навигации, если используется библиотека 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 и системным кодом представления. По возможности мы рекомендуем инкапсулировать это общее состояние в отдельный класс, соответствующий лучшим практикам использования пользовательских функций (UDF), применяемым на обеих платформах; например, в ViewModel , который предоставляет поток общих данных для генерации обновлений данных.
Однако это не всегда возможно, если передаваемые данные являются изменяемыми или тесно связаны с элементом пользовательского интерфейса. В этом случае одна система должна быть источником достоверной информации, и эта система должна передавать любые обновления данных другой системе. Как правило, источником достоверной информации должен быть тот элемент, который находится ближе к корню иерархии пользовательского интерфейса.
Творчество как источник истины
Используйте компонент SideEffect для публикации состояния Compose в код, не использующий Compose. В этом случае источник достоверной информации хранится в компоненте, который отправляет обновления состояния.
As an example, your analytics library might allow you to segment your user population by attaching custom metadata ( user properties in this example) to all subsequent analytics events. To communicate the user type of the current user to your analytics library, use SideEffect to update its value.
@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» .
Рассматривайте систему как источник истины.
Если система представлений владеет состоянием и делится им с 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, так и на экранах, созданных с помощью View.
В 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) является состоятельным (stateful). View управляет полями, которые описывают, что отображать, а также способ отображения. При преобразовании View в Compose следует стремиться к разделению отображаемых данных для достижения однонаправленного потока данных, как это более подробно объясняется в разделе «поднятие состояния» (state hoisting) .
Например, у 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 предлагает для создания адаптивных пользовательских интерфейсов.
Вложенная прокрутка с использованием Views
Для получения дополнительной информации о том, как включить вложенную прокрутку между прокручиваемыми элементами View и прокручиваемыми составными элементами, вложенными в обоих направлениях, ознакомьтесь с разделом «Вложенная прокрутка» .
Создание контента в RecyclerView
Начиная с версии RecyclerView 1.3.0-alpha02, производительность компонуемых элементов в RecyclerView значительно повысилась. Убедитесь, что у вас установлена как минимум версия 1.3.0-alpha02 RecyclerView , чтобы ощутить эти преимущества.
WindowInsets взаимодействуют с Views
В случае, если ваш экран содержит элементы View и Compose в одной иерархии, может потребоваться переопределить значения отступов по умолчанию. В этом случае необходимо явно указать, какой элемент должен использовать отступы, а какой — игнорировать.
Например, если ваш внешний макет — это макет Android View, вам следует использовать отступы в системе View и игнорировать их в Compose. В качестве альтернативы, если ваш внешний макет — это компонуемый объект, вам следует использовать отступы в Compose и соответствующим образом отполнять компонуемые объекты AndroidView .
По умолчанию каждый ComposeView использует все отступы на уровне WindowInsetsCompat . Чтобы изменить это поведение по умолчанию, установите ComposeView.consumeWindowInsets в false .
Для получения более подробной информации ознакомьтесь с документацией по WindowInsets в Compose .
Рекомендуем вам
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Отобразить эмодзи
- Material Design 2 в Compose
- Вставки окон в Compose