Inne uwagi

Chociaż przejście z Widoków na Compose jest związane wyłącznie z interfejsem użytkownika, należy wziąć pod uwagę wiele kwestii, aby przeprowadzić bezpieczną i stopniową migrację. Ta strona zawiera informacje, o których należy pamiętać podczas przenoszenia aplikacji opartej na View 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 wykorzystaniem biblioteki AppCompat (np. Theme.AppCompat.*)
  • Material Design 2 z użyciem 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 interfejsu 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 po 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, upewnij się, że przed każdym komponentem, który emituje interfejs z bibliotek Material Compose, stosujesz MaterialTheme. Składniki interfejsu Material (Button, Text itp.) zależą od tego, czy MaterialTheme jest aktywna. Bez niej ich działanie jest nieokreślone.

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

Więcej informacji znajdziesz w artykułach Systemy projektowania w ComposePrzenoszenie 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 aktywność lub fragment używa funkcji Compose, zamiast ActivityScenarioRule musisz użyć createAndroidComposeRule. 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()
    }
}

Aby dowiedzieć się więcej o testowaniu, przeczytaj artykuł Testowanie układu okna 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 dopasowuje się do Compose. Jeśli aplikacja używa innych wzorów architektury, np. Model View Presenter (MVP), zalecamy przeniesienie tej części interfejsu użytkownika do UDF przed wdrożeniem Compose lub w trakcie jego wdrażania.

Korzystanie z ViewModel w sekcji Tworzenie

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.

Podczas korzystania z kompozycji należy uważać, aby w różnych składanych elementach używać tego samego typu ViewModel, ponieważ elementy ViewModel są zgodne z zakresem 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 tym przykładzie ten sam użytkownik („user1”) jest witany dwukrotnie, ponieważ ta sama instancja GreetingViewModel jest używana ponownie we wszystkich składaniach w ramach hostowanej aktywności. 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ą celem w grafie nawigacyjnym, mają inną instancję elementu ViewModel. W tym przypadku ViewModel jest ograniczone do cyklu życia miejsca docelowego i jest usuwane, gdy miejsce docelowe zostanie usunięte z backstacka. W tym przykładzie, gdy użytkownik przechodzi na ekran Profil, tworzony jest nowy element 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. Zalecamy, aby w miarę możliwości otaczać ten stan w innej klasie, która stosuje się do sprawdzonych metod UDF używanych na obu platformach. Możesz to zrobić na przykład w klasie ViewModel, która udostępnia strumień współdzielonych danych, aby emitować aktualizacje danych.

Nie zawsze jest to jednak możliwe, jeśli dane, które mają być udostępniane, są zmienne lub ściśle powiązane z elementem interfejsu użytkownika. W takim przypadku jeden system musi być źródłem prawdy i musi udostępniać wszelkie aktualizacje danych do drugiego systemu. 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

Użyj komponentu SideEffect, aby opublikować stan Compose w kodzie niebędącym kodem Compose. 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 zmiennej SideEffect do zaktualizowania jej wartości.

@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 stan jest obsługiwany przez system View i jest udostępniany komponentowi Compose, zalecamy owinięcie stanu w obiekty mutableStateOf, aby zapewnić bezpieczeństwo wątku dla Compose. Jeśli zastosujesz to podejście, funkcje kompozytowe zostaną uproszczone, ponieważ nie będą już mieć źródła prawdy, ale system View musi zaktualizować stan zmienny i widoki, które go używają.

W tym przykładzie element CustomViewGroup zawiera element TextView i element ComposeView z elementem kompozytowym TextField. 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

Jeśli stopniowo przechodzisz na Compose, możesz potrzebować elementów interfejsu użytkownika w Compose i systemie 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 wspólne elementy interfejsu użytkownika stają się komponentami, których można używać w całej aplikacji niezależnie od tego, czy element ma styl za pomocą kodu XML, czy jest widokiem niestandardowym. Na przykład możesz utworzyć komponent CallToActionButton dla niestandardowego wezwania do działania Button.

Aby używać komponentu na ekranach opartych na widoku, utwórz niestandardowy element opakowujący widok, który rozszerza się z poziomu AbstractComposeView. W elementzie kompozycyjnym Content z przesłoniętym tematem 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 w widoku niestandardowym. 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 stan, który można zmienić, zapoznaj się z artykułem Źródło stanu.

Priorytetowe traktowanie stanu 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 prezentacji 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.

Promowanie opakowanych i wielorazowych komponentów

Elementy View często mają pewną wiedzę o tym, gdzie się znajdują: w elementach Activity, Dialog, Fragment lub gdzieś w hierarchii innego elementu View. Ponieważ są one często tworzone na podstawie statycznych plików układu, ogólna struktura pliku View jest zwykle 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 jakąś czynność. 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 kompozytorów z wielokrotnego użytku problem ten jest mniej dotkliwy. Rodzice mogą łatwo określać stan i wywołania zwrotne, dzięki czemu możesz pisać wielokrotnie użyte komponenty bez konieczności znajomości dokładnego miejsca ich użycia.

@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 są zagnieżdżone elementy ImageWithEnabledOverlay lub ControlPanelWithToggle. Dzieci mogą animować zmiany, zamieniać treści lub przekazywać je innym dzieciom.

Ten wzór nosi nazwę inwersja kontroli. Więcej informacji znajdziesz w dokumentacjiCompositionLocal.

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. Aby dowiedzieć się więcej, zapoznaj się z artykułem Używanie klas rozmiarów okien.

Dodatkowo zapoznaj się z artykułem Obsługa różnych rozmiarów ekranów, aby dowiedzieć się więcej o technikach, które Compose oferuje do tworzenia interfejsów adaptacyjnych.

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

Więcej informacji o włączaniu obsługi sterowania przewijaniem w głębokości między elementami widoku z możliwością przewijania i komponowanymi elementami z możliwością przewijania, które są ułożone w głębokości w obu kierunkach, znajdziesz w artykule Interoperacyjność elementów widoku z możliwością przewijania i komponowanych elementów z możliwością przewijania.

Napisz w RecyclerView

Komponenty w RecyclerView są wydajne od wersji RecyclerView 1.3.0-alpha02. 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 najszerszy układ jest układem Android View, powinieneś używać wstawek w systemie View i ignorować je w Compose. Jeśli natomiast zewnętrzny układ jest składanym elementem, musisz użyć w Compose wbudowanych elementów i odpowiednio uzupełnić składane elementy AndroidView.

Domyślnie każda 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.