Дополнительные рекомендации

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

При создании новых экранов в 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 .

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}