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

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

Если вы используете компонент «Навигация» в своем приложении, см. разделы «Навигация с помощью 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 с существующей архитектурой приложения

Шаблоны архитектуры Unidirectional Data Flow (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 ограничен жизненным циклом пункта назначения и очищается, когда пункт назначения удаляется из обратного стека. В следующем примере, когда пользователь переходит на экран Profile , создается новый экземпляр GreetingViewModel .

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

Государственный источник истины

При внедрении Compose в одной части пользовательского интерфейса может возникнуть необходимость в том, чтобы Compose и системный код View обменивались данными. Когда это возможно, мы рекомендуем вам инкапсулировать это общее состояние в другом классе, который следует лучшим практикам UDF, используемым обеими платформами; например, в ViewModel , который предоставляет поток общих данных для выпуска обновлений данных.

Однако это не всегда возможно, если данные, которыми нужно поделиться, изменяемы или тесно связаны с элементом пользовательского интерфейса. В этом случае одна система должна быть источником истины, и эта система должна делиться любыми обновлениями данных с другой системой. Как правило, источником истины должен владеть тот элемент, который ближе к корню иерархии пользовательского интерфейса.

Сочинение как источник истины

Используйте SideEffect composable для публикации состояния 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. Если вы используете этот подход, компонуемые функции упрощаются, поскольку у них больше нет источника истины, но система View должна обновить изменяемое состояние и Views, которые используют это состояние.

В следующем примере 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-based экранах.

В 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 надуваемым и пригодным для использования, как и традиционное представление. Ниже приведен пример этого с привязкой представления :

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 определенного типа с определенным id, и изменять его свойства напрямую в ответ на какое-то действие. Это тесно связывает эти элементы 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

Вам может потребоваться переопределить вставки по умолчанию, когда на вашем экране есть и Views, и Compose code в одной и той же иерархии. В этом случае вам нужно будет явно указать, какой из них должен потреблять вставки, а какой должен их игнорировать.

Например, если ваш внешний макет — это макет Android View, вы должны использовать вставки в системе View и игнорировать их для Compose. В качестве альтернативы, если ваш внешний макет — это компонуемый, вы должны использовать вставки в Compose и дополнять компонуемые AndroidView соответствующим образом.

По умолчанию каждый ComposeView потребляет все вставки на уровне потребления WindowInsetsCompat . Чтобы изменить это поведение по умолчанию, установите ComposeView.consumeWindowInsets в false .

Для получения дополнительной информации прочтите документацию WindowInsets в Compose .

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