Inne uwagi

Chociaż migracja z Widoki do Compose ma wyłącznie związek z interfejsem, należy wziąć pod uwagę wiele kwestii, aby przeprowadzić bezpieczną i stopniową migrację. Ta strona zawiera kilka kwestii, które warto wziąć pod uwagę podczas przenoszenia aplikacji opartej na widoku do Compose.

Migracja motywu aplikacji

Material Design to zalecany system projektowania motywów aplikacji na Androida.

W przypadku aplikacji opartych na widoku dostępne są 3 wersje interfejsu Material:

  • Material Design 1 z użyciem biblioteki AppCompat (np. Theme.AppCompat.*)
  • Material Design 2 za pomocą biblioteki MDC-Android (np. Theme.MaterialComponents.*)
  • Material Design 3 za pomocą biblioteki MDC-Android (np. Theme.Material3.*)

W przypadku aplikacji Compose są dostępne 2 wersje Material:

  • Material Design 2 za pomocą biblioteki Compose Material (np. androidx.compose.material.MaterialTheme)
  • Material Design 3 za pomocą biblioteki Compose Material 3 (np. androidx.compose.material3.MaterialTheme)

Zalecamy korzystanie z najnowszej wersji (Material 3), jeśli system projektowania aplikacji na to pozwala. Dostępne są przewodniki migracji dotyczące zarówno widoków, jak i komponowania:

Podczas tworzenia nowych ekranów w Compose, niezależnie od tego, której wersji Material Design używasz, przed każdym komponentem, który emituje interfejs z bibliotek Material Compose, zastosuj MaterialTheme. Komponenty Material (Button, Text itp.) zależą od zastosowania MaterialTheme, a bez niego ich działanie jest niezrozumiałe.

Wszystkie próbki Jetpack Compose korzystają z niestandardowego motywu Compose utworzonego na podstawie MaterialTheme.

Więcej informacji znajdziesz w artykułach Projektowanie systemów w Compose i Migracja motywów XML do Compose.

Jeśli w swojej aplikacji używasz komponentu Nawigacja, zapoznaj się z artykułami Nawigacja w Compose – interoperacyjnośćPrzenoszenie Jetpack Navigation do Nawigacji w Compose, aby uzyskać więcej informacji.

Testowanie interfejsu Compose/Views w trybie mieszanym

Po przeniesieniu części aplikacji do Compose konieczne jest przetestowanie jej, aby mieć pewność, że nic nie zostało zepsute.

Jeśli działanie lub fragment korzysta z funkcji Utwórz, musisz użyć funkcji createAndroidComposeRule zamiast ActivityScenarioRule. createAndroidComposeRule integruje ActivityScenarioRule z ComposeTestRule, co pozwala testować kod w Compose i View jednocześnie.

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()
    }
}

Więcej informacji o testowaniu znajdziesz w artykule Testowanie układu tworzenia wiadomości. Informacje o współdziałaniu z platformami testowania interfejsu użytkownika znajdziesz w artykułach Interoperencja z EspressoInteroperencja z UiAutomator.

Integracja Compose z dotychczasową architekturą aplikacji

Przepływ danych w jednym kierunku (UDF) w architekturze do komponowania działa bezproblemowo. Jeśli aplikacja korzysta z innych typów wzorców architektury, np. prezentacji modelu widoku modelu (MVP), zalecamy przeniesienie tej części interfejsu użytkownika do UDF przed wdrożeniem funkcji tworzenia wiadomości lub w trakcie wdrażania tej funkcji.

Korzystanie z ViewModel w Compose

Jeśli używasz biblioteki Architecture ComponentsViewModel, możesz uzyskać dostęp do funkcji ViewModel z dowolnego komponentu, wywołując funkcję viewModel(), jak opisano w Compose i innych bibliotekach.

Gdy wdrażasz funkcję Compose, zachowaj ostrożność, aby używać tego samego typu ViewModel w różnych funkcjach kompozycyjnych, ponieważ elementy ViewModel są zgodne z zakresami cyklu życia widoku. Zakres może obejmować aktywność hosta, fragment lub graf nawigacji, jeśli używana jest biblioteka nawigacji.

Jeśli na przykład komponenty są hostowane w aktywności, viewModel() zawsze zwraca tę samą instancję, która jest usuwana dopiero po zakończeniu aktywności. W poniższym przykładzie ten sam użytkownik („user1”) jest powitany dwukrotnie, ponieważ to samo wystąpienie GreetingViewModel jest ponownie używane we wszystkich elementach kompozycyjnych w ramach działania hosta. Pierwsza utworzona instancja ViewModel jest używana ponownie w innych składanych komponentach.

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
    }
}

Ponieważ grafy nawigacyjne obejmują też elementy ViewModel, komponenty, które są miejscem docelowym w grafie nawigacyjnym, mają inną instancję elementu ViewModel. W takim przypadku pole ViewModel jest ograniczone do cyklu życia miejsca docelowego i jest czyszczone, gdy miejsce docelowe zostanie usunięte z backstacku. W tym przykładzie, gdy użytkownik przechodzi na ekran Profil, tworzony jest nowy obiekt GreetingViewModel.

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

Stan źródła danych

Gdy zastosujesz funkcję Compose w jednej części interfejsu użytkownika, może się okazać, że funkcja Compose i kod systemu View muszą udostępniać dane. Jeśli to możliwe, zalecamy uwzględnienie tego wspólnego stanu w innej klasie zgodnej ze sprawdzonymi metodami dotyczącymi UDF wykorzystywanych przez obie platformy, na przykład w obiekcie ViewModel, który ujawnia strumień udostępnionych danych w celu aktualizacji danych.

Jednak nie zawsze jest to możliwe, jeśli dane, które mają być udostępniane, są zmienne lub są ściśle powiązane z elementem interfejsu. W takim przypadku jeden system musi być źródłem danych, a drugi musi udostępniać aktualizacje danych drugiemu. Z zasady źródło danych podstawowych powinno być własnością elementu, który znajduje się bliżej korzenia hierarchii interfejsu.

Tworzenie jako źródło danych

Aby opublikować stan tworzenia w kodzie, który nie służy do tworzenia wiadomości, użyj funkcji kompozycyjnej SideEffect. W tym przypadku źródło informacji jest przechowywane w komponowalnym elemencie, który wysyła aktualizacje stanu.

Na przykład biblioteka analityczna może umożliwiać podział populacji użytkowników na segmenty przez dołączanie niestandardowych metadanych (w tym przykładzie są to właściwości użytkownika) do wszystkich kolejnych zdarzeń analitycznych. Aby przekazać typ użytkownika do biblioteki analitycznej, użyj parametru SideEffect, aby zaktualizować jego wartość.

@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
}

Więcej informacji znajdziesz w artykule Efekty uboczne w Compose.

System jako źródło informacji

Jeśli system widoku danych jest właścicielem stanu i udostępnia go usłudze Compose, zalecamy opakowanie stanu w obiektach mutableStateOf, tak aby w przypadku tworzenia wiadomości stan był bezpieczny w wątkach. Jeśli używasz tego podejścia, funkcje kompozycyjne są uproszczone, ponieważ nie mają już źródła danych, ale system widoków musi zaktualizować stan zmienny i widoki korzystające z tego stanu.

W tym przykładzie CustomViewGroup zawiera TextView i ComposeView z funkcją TextField kompozycyjną w środku. TextView musi wyświetlać treści wpisywane przez użytkownika w polu 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
    }
}

Migracja wspólnego interfejsu użytkownika

Jeśli stopniowo przechodzisz na Compose, możesz potrzebować elementów interfejsu użytkownika wspólnych dla Compose i systemu View. Jeśli na przykład Twoja aplikacja zawiera niestandardowy komponent CallToActionButton, może być konieczne użycie go zarówno na ekranie tworzenia, jak i na ekranie wyświetlania.

W Compose udostępnione elementy interfejsu stają się elementami kompozycyjnymi, których można ponownie używać w aplikacji, niezależnie od tego, czy element ma określony styl za pomocą XML, czy jest widokiem niestandardowym. Na przykład możesz utworzyć komponent CallToActionButton dla niestandardowego wezwania do działania Button.

Aby używać funkcji kompozycyjnej na ekranach opartych na widoku, utwórz niestandardowy kod widoku danych rozciągający się od AbstractComposeView. W elementzie kompozycyjnym Content z zastąpionymi uprawnieniami umieść element kompozycyjny utworzony w ramach motywu aplikacji, jak pokazano w przykładzie poniżej:

@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)
        }
    }
}

Zwróć uwagę, że parametry kompozytowe stają się zmiennymi zmiennymi wewnątrz widoku niestandardowego. Dzięki temu widok niestandardowy CallToActionViewButton będzie można rozszerzać i używać jak tradycyjny widok. Poniżej znajdziesz przykład korzystania z wiązania widoku:

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 */ }
        }
    }
}

Jeśli komponent niestandardowy zawiera zmienny stan, zapoznaj się z sekcją Stan źródła danych.

Nadaj priorytet stanowi podziału z prezentacji

Tradycyjnie View ma stan. View zarządza polami, które opisują co wyświetlić, a także jak to zrobić. Podczas konwertowania View na kompozycję pamiętaj o oddzieleniu danych renderowanych, aby uzyskać jednokierunkowy przepływ danych. Więcej informacji znajdziesz w artykule Przenoszenie stanu.

Na przykład obiekt View ma właściwość visibility, która określa, czy jest on widoczny, niewidoczny czy usunięty. Jest to nieodłączna właściwość View. Chociaż inne fragmenty kodu mogą zmieniać widoczność View, tylko View wie, jaka jest jego bieżąca widoczność. Logika, która zapewnia widoczność View, może być podatna na błędy i często jest powiązana z samym elementem View.

Z drugiej strony, Compose ułatwia wyświetlanie zupełnie innych komponentów za pomocą logiki warunkowej w Kotlinie:

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    if (showCautionIcon) {
        CautionIcon(/* ... */)
    }
}

Z założenia CautionIcon nie musi wiedzieć, dlaczego jest wyświetlany, ani się tym przejmować. Nie ma też pojęcia visibility: albo jest ona widoczna w składnicy, albo nie.

Dzięki wyraźnemu oddzieleniu zarządzania stanem od logiki wyświetlania możesz dowolnie zmieniać sposób wyświetlania treści jako konwersji stanu na interfejsie. Możliwość podniesienia stanu w razie potrzeby zwiększa też możliwość wielokrotnego używania komponentów, ponieważ własność stanu jest bardziej elastyczna.

Promuj umieszczone w opakowaniu komponenty i komponenty wielokrotnego użytku

Elementy View często mają pewną wiedzę o tym, gdzie się znajdują: w elementach Activity, Dialog, Fragment lub gdzieś w hierarchii innego elementu View. W plikach układu statycznego często są one zawyżone, więc ogólna struktura elementu View bywa bardzo sztywna. Spowoduje to ściślejsze powiązanie i utrudni zmianę lub ponowne użycie View.

Na przykład element niestandardowy View może zakładać, że ma widok podrzędny określonego typu z określonym identyfikatorem i zmieniać jego właściwości bezpośrednio w odpowiedzi na działanie. Elementy View są ze sobą ściśle powiązane: niestandardowy element View może się zawiesić lub ulec uszkodzeniu, jeśli nie znajdzie elementu podrzędnego, a element podrzędny prawdopodobnie nie będzie można ponownie użyć bez niestandardowego elementu nadrzędnego View.

W przypadku funkcji kompozycyjnych wielokrotnego użytku nie jest to problemem. Rodzice mogą łatwo określać stan i wywołania zwrotne, dzięki czemu możesz pisać elementy kompozycyjne wielokrotnego użytku bez konieczności podawania dokładnego miejsca, w którym będą używane.

@Composable
fun AScreen() {
    var isEnabled by rememberSaveable { mutableStateOf(false) }

    Column {
        ImageWithEnabledOverlay(isEnabled)
        ControlPanelWithToggle(
            isEnabled = isEnabled,
            onEnabledChanged = { isEnabled = it }
        )
    }
}

W przykładzie powyżej wszystkie 3 części są bardziej odizolowane i mniej powiązane:

  • ImageWithEnabledOverlay musi tylko znać bieżący stan isEnabled. Nie musi wiedzieć, że ControlPanelWithToggle istnieje, ani jak można go kontrolować.

  • ControlPanelWithToggle nie wie, że ImageWithEnabledOverlay istnieje. Może istnieć zero, jeden lub więcej sposobów wyświetlania isEnabled, a ControlPanelWithToggle nie musi się zmieniać.

  • Dla elementu nadrzędnego nie ma znaczenia, jak głęboko ImageWithEnabledOverlay lub ControlPanelWithToggle są zagnieżdżone. Dzieci mogą animować zmiany, zamieniać treści lub przekazywać je innym dzieciom.

Ten wzorzec nosi nazwę odwrócenia kontroli. Więcej informacji na ten temat znajdziesz w dokumentacji CompositionLocal.

Obsługa zmian rozmiaru ekranu

Użycie różnych zasobów w zależności od rozmiaru okna to jeden z głównych sposobów tworzenia elastycznych układów View. Chociaż zasobów z kwalifikacją nadal można używać do podejmowania decyzji dotyczących układu na poziomie ekranu, kompozytor znacznie ułatwia zmianę układów wyłącznie w kodzie za pomocą zwykłej logiki warunkowej. Więcej informacji znajdziesz w artykule Używanie klas rozmiaru okna.

Dodatkowo w artykule Obsługa różnych rozmiarów ekranu znajdziesz techniki, które umożliwiają tworzenie adaptacyjnych interfejsów użytkownika przez Compose.

Zagnieżdżone przewijanie za pomocą widoków

Więcej informacji o włączaniu obsługi sterowania przewijaniem w głębokości między przewijalnymi elementami widoku i przewijalnymi komponentami, które są w głębokości po obu stronach, znajdziesz w artykule Interoperacyjność elementów widoku z przewijaniem w głębokości.

Napisz w RecyclerView

Elementy kompozycyjne w RecyclerView są wydajniejsze od RecyclerView w wersji 1.3.0-alfa02. Aby skorzystać z tych funkcji, musisz mieć co najmniej wersję 1.3.0-alpha02 aplikacji RecyclerView.

WindowInsets współpraca z widokami

Jeśli na ekranie znajdują się zarówno widoki, jak i kod Compose w ramach tej samej hierarchii, może być konieczne zastąpienie domyślnych wstawek. W takim przypadku musisz wyraźnie określić, która z nich powinna używać wstawek, a która powinna je ignorować.

Jeśli na przykład Twój najbardziej zewnętrzny układ to układ Android View, podczas tworzenia wiadomości używaj wstawienia w systemie widoku i zignoruj je. Jeśli Twój najbardziej zewnętrzny układ jest kompozycyjny, użyj też zestawów instalacyjnych w funkcji Compose i dodaj odpowiednie elementy kompozycyjne AndroidView.

Domyślnie każdy element ComposeView zużywa wszystkie wstawione elementy na poziomie zużycia WindowInsetsCompat. Aby zmienić to domyślne działanie, ustaw wartość ComposeView.consumeWindowInsets na false.

Więcej informacji znajdziesz w dokumentacji dotyczącej WindowInsets w Compose.