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

Хотя переход от 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 ).

Мы рекомендуем использовать последнюю версию (Материал 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 с существующей архитектурой вашего приложения

Архитектурные шаблоны однонаправленного потока данных (UDF) без проблем работают с Compose. Если вместо этого приложение использует другие типы архитектурных шаблонов, например Model View Presenter (MVP), мы рекомендуем перенести эту часть пользовательского интерфейса в UDF до или во время внедрения Compose.

Использование ViewModel в Compose

Если вы используете библиотеку ViewModel компонентов архитектуры , вы можете получить доступ к ViewModel из любого компонуемого объекта, вызвав функцию viewModel() , как описано в 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 должны обмениваться данными. Когда это возможно, мы рекомендуем вам инкапсулировать это общее состояние в другой класс, который соответствует лучшим практикам 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. Если вы используете этот подход, составные функции упрощаются, поскольку у них больше нет источника истины, но системе представлений необходимо обновить изменяемое состояние и представления, которые используют это состояние.

В следующем примере 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 общие элементы пользовательского интерфейса становятся составными элементами, которые можно повторно использовать в приложении независимо от того, стилизован ли элемент с использованием 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 управляет полями, которые описывают , что отображать, а также то, как это отображать. Когда вы конвертируете 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 .

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