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

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

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

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}