Inne uwagi

Chociaż migracja z widoków danych do tworzenia wiąże się wyłącznie z interfejsem użytkownika, trzeba wziąć pod uwagę wiele kwestii, aby przeprowadzić bezpieczną i przyrostową migrację. Na tej stronie znajdziesz kilka uwag na temat migracji aplikacji opartej na widokach do usługi Compose.

Migracja motywu aplikacji

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

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

  • Material Design 1 z biblioteką AppCompat (np. Theme.AppCompat.*)
  • Material Design 2 z biblioteką MDC-Android (np. Theme.MaterialComponents.*)
  • Material Design 3 z biblioteką MDC-Android (np. Theme.Material3.*)

W przypadku aplikacji do tworzenia wiadomości dostępne są 2 wersje Material Design:

  • użyj stylu Material Design 2, korzystając z biblioteki Compose Material (np.androidx.compose.material.MaterialTheme).
  • Material Design 3 z biblioteką Compose Material 3 (np. androidx.compose.material3.MaterialTheme)

Jeśli system projektowania aplikacji może to zrobić, zalecamy korzystanie z najnowszej wersji (Material 3). Dostępne są przewodniki po migracji dotyczące zarówno widoków, jak i tworzenia:

Gdy tworzysz nowe ekrany w interfejsie Compose, niezależnie od używanej wersji interfejsu Material Design, pamiętaj, aby zastosować element MaterialTheme przed elementami kompozycyjnymi, które generują interfejs użytkownika z bibliotek Material Design. Komponenty Material (Button, Text itd.) zależą od tego, czy istnieje MaterialTheme, a bez niego ich zachowanie jest niezdefiniowane.

Wszystkie przykłady usługi Jetpack Compose korzystają z niestandardowego motywu tworzenia utworzonego na podstawie szablonu MaterialTheme.

Więcej informacji znajdziesz w sekcjach Projektowanie systemów w interfejsie API i Migracja motywów XML do tworzenia wiadomości.

Jeśli korzystasz w aplikacji z komponentu Nawigacja, zapoznaj się z artykułami Nawigacja z użyciem funkcji tworzenia wiadomości – interoperacyjność i Migracja z nawigacji Jetpack do Nawigacji w celu tworzenia wiadomości, aby dowiedzieć się więcej.

Testowanie mieszanego interfejsu tworzenia i widoku obiektów

Po przeniesieniu niektórych elementów aplikacji do usługi Compose musisz sprawdzić, czy nic nie jest uszkodzone.

Jeśli aktywność lub fragment używają funkcji Utwórz, musisz użyć właściwości createAndroidComposeRule zamiast ActivityScenarioRule. createAndroidComposeRule integruje sięActivityScenarioRule z ComposeTestRule, który umożliwia jednoczesne testowanie tworzenia i wyświetlania kodu.

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 na temat testowania znajdziesz w artykule Testowanie układu tworzenia wiadomości. Informacje o zgodności ze platformami do testowania interfejsu użytkownika znajdziesz w artykułach o interoperacyjności z Espresso i interoperacyjności z UiAutomator.

Integracja funkcji tworzenia wiadomości z istniejącą architekturą aplikacji

Wzorce architektury jednokierunkowego przepływu danych (UDF) bezproblemowo współpracują z funkcją tworzenia. Jeśli aplikacja używa innych typów wzorców architektury, takich jak prezenter widoku modelu (MVP), zalecamy przeniesienie tej części interfejsu do UDF przed wdrożeniem tworzenia wiadomości lub w trakcie jego konfigurowania.

Używanie elementu ViewModel w funkcji Utwórz

Jeśli używasz biblioteki Komponenty architekturyViewModel, możesz uzyskać dostęp do elementu ViewModel z dowolnego elementu kompozycyjnego, wywołując funkcję viewModel(), jak opisano w sekcji Tworzenie i biblioteki.

Przyjmując opcję tworzenia, pamiętaj o używaniu tego samego typu ViewModel w różnych obiektach kompozycyjnych, ponieważ elementy ViewModel są zgodne z zakresami cyklu życia widoków. Zakresem będzie aktywność hosta, fragment lub wykres nawigacyjny (jeśli jest używana biblioteka nawigacji).

Jeśli na przykład elementy kompozycyjne są hostowane w aktywności, viewModel() zawsze zwraca tę samą instancję, która jest wyczyszczona dopiero po zakończeniu działania. W poniższym przykładzie ten sam użytkownik („user1”) jest powitany dwukrotnie, ponieważ ta sama instancja GreetingViewModel jest ponownie używana we wszystkich elementach kompozycyjnych w ramach aktywności hosta. Pierwsze utworzone wystąpienie ViewModel jest ponownie używane w innych funkcjach kompozycyjnych.

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ż wykresy nawigacyjne obejmują również elementy ViewModel, obiekty kompozycyjne, które są miejscem docelowym na wykresie nawigacyjnym, mają inne wystąpienie elementu ViewModel. W tym przypadku obiekt ViewModel jest ograniczony do cyklu życia miejsca docelowego i jest czyszczony po usunięciu miejsca docelowego ze stosu. W poniższym przykładzie, gdy użytkownik przejdzie do ekranu Profil, zostanie utworzona nowa instancja obiektu GreetingViewModel.

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

Stanowe źródło wiarygodnych informacji

Gdy wdrożysz tworzenie w jednej części interfejsu użytkownika, może się zdarzyć, że kod systemowy tworzenia i widoku będzie współdzielić dane. Zalecamy, aby w miarę możliwości umieścić ten udostępniony stan w innej klasie zgodnej ze sprawdzonymi metodami dotyczącymi UDF używanymi przez obie platformy, np. w obiekcie ViewModel, który ujawnia strumień udostępnionych danych do emisji aktualizacji danych.

Nie zawsze jest to jednak możliwe, jeśli udostępniane dane są zmienne lub są ściśle powiązane z elementem interfejsu. W takim przypadku źródłem wiarygodnych danych musi być jeden z systemów, który będzie udostępniać aktualizacje danych drugiemu. Podstawową zasadą jest to, że źródło prawdziwości powinno należeć do elementu, który znajduje się bliżej poziomu głównego hierarchii interfejsu.

Utwórz jako źródło wiarygodnych informacji

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

Na przykład biblioteka statystyk może umożliwiać segmentowanie użytkowników, dołączając niestandardowe metadane (w tym przykładzie właściwości użytkownika) do wszystkich kolejnych zdarzeń Analytics. Aby przekazać informacje o typie bieżącego użytkownika do biblioteki Analytics, zaktualizuj jego wartość za pomocą funkcji 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
}

Więcej informacji znajdziesz w artykule Efekty uboczne przy tworzeniu wiadomości.

Wyświetl system jako źródło wiarygodnych informacji

Jeśli system widoku danych jest właścicielem stanu i udostępnia go funkcji Compose, zalecamy spakowanie go w obiekty mutableStateOf, aby można go było bezpiecznie używać w wątkach. Jeśli korzystasz z tego podejścia, funkcje kompozycyjne zostaną uproszczone, ponieważ nie mają już one źródła informacji, ale system widoków musi zaktualizować stan zmienny i widoki, które z niego korzystają.

W poniższym przykładzie obiekt CustomViewGroup zawiera elementy kompozycyjne TextView i ComposeView, które zawierają element kompozycyjny TextField. Element TextView musi wyświetlać zawartość tego, co użytkownik wpisuje 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ółdzielonego interfejsu

Jeśli przechodzisz do tworzenia wiadomości stopniowo, konieczne może być używanie udostępnionych elementów interfejsu zarówno w systemie tworzenia, jak i w widoku widoku. Jeśli na przykład aplikacja ma niestandardowy komponent CallToActionButton, może być konieczne używanie go zarówno na ekranie tworzenia, jak i na ekranach opartych na widokach.

W widoku tworzenia udostępnione elementy interfejsu stają się obiektami kompozycyjnymi, których można używać wielokrotnie w aplikacji niezależnie od tego, czy styl elementu jest określony za pomocą kodu XML, czy też jest to widok niestandardowy. Możesz na przykład utworzyć kompozycję CallToActionButton dla komponentu niestandardowego wezwania do działania Button.

Aby używać funkcji kompozycyjnej na ekranach opartych na widokach, utwórz kod widoku niestandardowego, który rozciąga się od AbstractComposeView. W zastąpionym elemencie Content kompozycyjnym umieść utworzony kompozycję zapakowaną w motywie tworzenia, jak 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 kompozycyjne stają się zmiennymi w widoku niestandardowym. Dzięki temu niestandardowy widok CallToActionViewButton jest nadmuchiwany i można go używać jak tradycyjny widok. Zapoznaj się z poniższym przykładem w ramach powią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 ma zmienny stan, zapoznaj się z sekcją Stan – źródło informacji.

Nadaj priorytet stanowi podziału z prezentacji

Zazwyczaj View jest stanowy. Element View zarządza polami, które określają, co ma być wyświetlane, a także jak to wyświetlać. Gdy zmieniasz element View w tworzenie, rozłącz renderowane dane, aby osiągnąć jednokierunkowy przepływ danych, jak opisano dokładniej w sekcji Przenoszenie stanów.

Na przykład element View ma właściwość visibility, która określa, czy jest widoczna, niewidoczna czy już nie istnieje. Jest to nieodłączna właściwość View. Inne fragmenty kodu mogą zmieniać widoczność elementu View, ale tylko View tak naprawdę wie, jaka jest jego aktualna widoczność. Logika zapewniająca, że element View jest widoczny, może być podatny na błędy i często jest powiązany z tym samym obiektem View.

Natomiast funkcja Compose ułatwia wyświetlanie zupełnie różnych elementów kompozycyjnych za pomocą logiki warunkowej w Kotlin:

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

Z założenia CautionIcon nie musi wiedzieć, dlaczego jest wyświetlany. Nie ma też znaczenia visibility – element znajduje się w kompozycji, albo nie.

Dzięki czystej oddzieleniu zarządzania stanem od logiki prezentacji możesz swobodniej zmieniać sposób wyświetlania treści jako konwersji stanu na interfejs. Możliwość podniesienia w razie potrzeby sprawia też, że kompozycje są bardziej podatne na wiele sposobów, ponieważ własność stanowa jest bardziej elastyczna.

Promuj zamknięte komponenty i komponenty wielokrotnego użytku

Elementy View często mają pewne wyobrażenie o tym, gdzie znajdują się: w obrębie Activity, Dialog, Fragment lub gdzieś w innej hierarchii View. Często są one wydłużane z plików z układem statycznym, więc ogólna struktura elementu View jest zwykle bardzo sztywna. Powoduje to ściślejsze sprzężenie i utrudnia zmianę lub ponowne użycie obiektu View.

Niestandardowy element View może np. przyjąć, że ma widok podrzędny określonego typu o określonym identyfikatorze, i w odpowiedzi na jakieś działanie bezpośrednio zmienić jego właściwości. To ściśle łączy te elementy View: niestandardowy element View może ulec awarii lub zostać uszkodzony, jeśli nie będzie mógł znaleźć elementu podrzędnego. Prawdopodobnie nie będzie można go użyć ponownie bez niestandardowego elementu nadrzędnego View.

Ten problem nie dotyczy już funkcji tworzenia wiadomości dzięki elementom kompozycyjnym wielokrotnego użytku. Elementy nadrzędne mogą łatwo określać stan i wywołania zwrotne, dzięki czemu możesz tworzyć elementy kompozycyjne wielokrotnego użytku bez konieczności poznania dokładnego miejsca, w którym będą one 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 zamknięte i rzadziej sprzężone:

  • ImageWithEnabledOverlay potrzebuje tylko informacji o bieżącym stanie isEnabled. Nie musi wiedzieć, że ControlPanelWithToggle istnieje, a nawet jak można go kontrolować.

  • ControlPanelWithToggle nie wie, że ImageWithEnabledOverlay istnieje. Może istnieć zero, 1 lub więcej sposobów wyświetlania elementu isEnabled i usługa ControlPanelWithToggle nie będzie musiała się zmieniać.

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

Ten wzorzec jest nazywany odwracaniem kontroli. Więcej informacji na ten temat znajdziesz w dokumentacji usługi CompositionLocal.

Obsługa zmian rozmiaru ekranu

Posiadanie różnych zasobów dla różnych rozmiarów okien to jeden z głównych sposobów tworzenia elastycznych układów View. Wykwalifikowane zasoby są nadal opcją przy podejmowaniu decyzji o układzie na poziomie ekranu, ale funkcja tworzenia znacznie ułatwia całkowite zmienianie układów w kodzie za pomocą zwykłej logiki warunkowej. Więcej informacji znajdziesz w artykule Obsługa różnych rozmiarów ekranu.

Poza tym znajdziesz w artykule Tworzenie elastycznych układów, aby poznać techniki dostępne w komponencie do tworzenia adaptacyjnych interfejsów użytkownika.

Zagnieżdżone przewijanie z widokami

Więcej informacji o tym, jak włączyć zagnieżdżone przewijanie między elementami widoku danych i przewijanymi elementami kompozycyjnymi, które są zagnieżdżone w obu kierunkach, znajdziesz w artykule Interoperacyjność z zagnieżdżonym przewijaniem.

Utwórz wiadomość w języku: RecyclerView

Elementy kompozycyjne w języku RecyclerView są wydajne od wersji RecyclerView w wersji 1.3.0-alfa02. Aby zobaczyć te korzyści, musisz korzystać z RecyclerView w wersji co najmniej 1.3.0-alfa02.

WindowInsets – interoperacyjność z widokami

Zastąpienie ustawień domyślnych może być konieczne, gdy ekran zawiera tę samą hierarchię widoków danych i kod tworzenia. W takim przypadku musisz wyraźnie wskazać, który z nich ma korzystać z wstawek, a który ignorować.

Jeśli na przykład najbardziej zewnętrznym układem jest układ Android View, musisz wykorzystać wstawione elementy z systemu Widok i zignorować je przy tworzeniu wiadomości. Jeśli najbardziej zewnętrzny układ to funkcja kompozycyjna, musisz wykorzystać wstawki w komponencie i odpowiednio dopełnić funkcje kompozycyjne AndroidView.

Domyślnie każdy element ComposeView wykorzystuje wszystkie wstawki na poziomie wykorzystania WindowInsetsCompat. Aby zmienić to domyślne działanie, ustaw parametr ComposeView.consumeWindowInsets na false.

Więcej informacji znajdziesz w dokumentacji WindowInsets w sekcji Tworzenie wiadomości.