Переход с 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).
Если система проектирования вашего приложения позволяет это сделать, мы рекомендуем использовать последнюю версию (Material 3). Для перехода с Views и Compose доступны руководства по переносу данных:
- Материал 1 в Материал 2 в представлениях
- Переход с Material 2 на Material 3 в представлениях
- Переход с Material 2 на Material 3 в Compose
При создании новых экранов в Compose, независимо от того, какую версию Material Design вы используете, убедитесь, что вы применяете MaterialTheme перед любыми композициями, которые генерируют элементы интерфейса из библиотек Compose Material. Компоненты Material (Button, Text и т. д.) зависят от элемента MaterialTheme, и без него их поведение не определено.
Во всех примерах Jetpack Compose используется специальная тема Compose, созданная на основе MaterialTheme.
Подробнее о системах дизайна в Compose и переносе тем XML в Compose…
Навигация
Если вы используете в приложении компонент навигации, ознакомьтесь с информацией в статьях Навигация из Compose в приложении на основе фрагментов и Переход с Jetpack Navigation на Navigation Compose.
Как протестировать смешанный интерфейс Compose/Views
После переноса части приложения в Compose тестирование необходимо, чтобы убедиться, что ничего не сломалось.
Если в действии или фрагменте используется Compose, вместо ActivityScenarioRule нужно использовать createAndroidComposeRule. createAndroidComposeRule интегрирован с ActivityScenarioRule, который позволяет тестировать код Compose и View одновременно.ComposeTestRule
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 при создании письма
Если вы используете библиотеку компонентов архитектурыViewModel ViewModel, то можете получить доступ к viewModel() из любой composable-функции, вызвав функцию 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 обменивались данными. По возможности мы рекомендуем инкапсулировать общее состояние в другом классе, который соответствует рекомендациям по использованию пользовательских функций, применяемым на обеих платформах. Например, можно использовать ViewModel, который предоставляет поток общих данных для передачи обновлений.
Однако это не всегда возможно, если данные, которыми нужно поделиться, изменяются или тесно связаны с элементом интерфейса. В таком случае одна из систем должна быть источником достоверной информации и передавать все обновления данных в другую систему. Как правило, источник истины должен принадлежать элементу, который находится ближе к корню иерархии интерфейса.
Compose как источник достоверных данных
Используйте composable-функцию SideEffect, чтобы опубликовать состояние 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. Если вы используете этот подход, то composable-функции упрощаются, поскольку у них больше нет источника истины. Однако системе View необходимо обновлять изменяемое состояние и представления, которые используют это состояние.
В примере ниже контейнер CustomViewGroup содержит элементы TextView и ComposeView, внутри которого находится composable-функция 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 composable для своего компонента Button.
Чтобы использовать composable-функцию на экранах на основе представлений, создайте специальный контейнер, который наследуется от AbstractComposeView. В переопределенной composable-функции Content разместите созданную вами composable-функцию, обернутую в тему 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: он либо есть в композиции, либо его нет.
Четко разделив управление состоянием и логику представления, вы сможете более свободно менять способ отображения контента как преобразование состояния в интерфейс. Возможность поднимать состояние при необходимости также делает функции, созданные с помощью Compose, более универсальными, поскольку владение состоянием становится более гибким.
Продвигайте инкапсулированные и многократно используемые компоненты
Элементы 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
Подробнее о том, как включить вложенную прокрутку для элементов View и composable-функций, вложенных в обоих направлениях, можно узнать в разделе Вложенная прокрутка.
Написать письмо в приложении "RecyclerView".
Компонуемые функции в RecyclerView работают эффективно, начиная с версии RecyclerView 1.3.0-alpha02. Чтобы воспользоваться этими преимуществами, убедитесь, что у вас установлена версия RecyclerView не ниже 1.3.0-alpha02.
WindowInsets совместимость с режимами просмотра
Вам может потребоваться переопределить вставки по умолчанию, если на экране есть как представления, так и код Compose в одной иерархии. В этом случае вам нужно явно указать, какой из них должен учитывать отступы, а какой – игнорировать их.
Например, если внешний макет представляет собой макет View для Android, вы должны использовать вставки в системе View и игнорировать их в Compose.
Если внешний макет является composable-функцией, то отступы следует использовать в Compose и соответствующим образом дополнять AndroidViewcomposable-функции.
По умолчанию каждый ComposeView потребляет все вставки на уровне потребления WindowInsetsCompat. Чтобы изменить режим работы по умолчанию, задайте для параметра ComposeView.consumeWindowInsets значение false.
Подробнее о WindowInsets в Compose…
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Показывать эмодзи
- Material Design 2 в Compose
- Внутренние отступы окна в режиме создания