Inne uwagi

Migracja z widoków do Compose dotyczy wyłącznie interfejsu, ale aby przeprowadzić bezpieczną i stopniową migrację, trzeba wziąć pod uwagę wiele kwestii. Na tej stronie znajdziesz kilka kwestii, które warto wziąć pod uwagę podczas przenoszenia aplikacji opartej na widokach do Compose.

Migracja motywu aplikacji

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

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

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

  • Material Design 2 z biblioteką 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 na to pozwala, zalecamy używanie najnowszej wersji (Material 3). Dostępne są przewodniki migracji zarówno dla widoków, jak i Compose:

Podczas tworzenia nowych ekranów w Compose, niezależnie od używanej wersji Material Design, przed użyciem jakichkolwiek elementów kompozycyjnych, które emitują interfejs z bibliotek Compose Material, zastosuj MaterialTheme. Komponenty Material Design (Button, Text itp.) wymagają zastosowania MaterialTheme, a bez niego ich działanie jest nieokreślone.

Wszystkie przykłady Jetpack Compose używają niestandardowego motywu Compose opartego na MaterialTheme.

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

Jeśli w aplikacji używasz komponentu Navigation, więcej informacji znajdziesz w artykułach Nawigacja z Compose w aplikacji opartej na fragmentach i Migracja Jetpack Navigation do Navigation Compose.

Testowanie mieszanego interfejsu Compose/Views

Po przeniesieniu części aplikacji do Compose testowanie jest niezbędne, aby upewnić się, że nic nie zostało uszkodzone.

Gdy aktywność lub fragment używa Compose, musisz użyć createAndroidComposeRule zamiast użyć ActivityScenarioRule. createAndroidComposeRule integruje ActivityScenarioRule z ComposeTestRule, co pozwala testować kod Compose i View w tym samym czasie.

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 Compose. Informacje o interoperacyjności z platformami testowania interfejsu znajdziesz w artykułach Interoperacyjność z Espresso i Interoperacyjność z UiAutomator.

Integracja Compose z istniejącą architekturą aplikacji

Wzorce architekturyjednokierunkowego przepływu danych (UDF) bezproblemowo współpracują z Compose. Jeśli aplikacja używa innych typów wzorców architektury, np. Model View Presenter (MVP), zalecamy przeniesienie tej części interfejsu do UDF przed lub w trakcie wdrażania Compose.

Używanie ViewModel w Compose

Jeśli używasz biblioteki komponentów architektury ViewModel, możesz uzyskać dostęp do ViewModel z dowolnego elementu kompozycyjnego, wywołując funkcję viewModel() , jak opisano w artykule Compose i inne biblioteki.

Podczas wdrażania Compose uważaj na używanie tego samego typu ViewModel w różnych elementach kompozycyjnych, ponieważ elementy ViewModel są zgodne z zakresami cyklu życia widoku. Zakres będzie aktywnością hosta, fragmentem lub wykresem nawigacji, jeśli używana jest biblioteka Navigation.

Jeśli na przykład elementy kompozycyjne są hostowane w aktywności, viewModel() zawsze zwraca tę samą instancję, która jest czyszczona tylko po zakończeniu aktywności. W poniższym przykładzie ten sam użytkownik („user1”) jest witany 2 razy, ponieważ ta sama instancja GreetingViewModel jest używana we wszystkich elementach kompozycyjnych w aktywności hosta. Pierwsza utworzona instancja ViewModel jest używana w innych elementach 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 nawigacji również obejmują elementy ViewModel, elementy kompozycyjne, które są miejscem docelowym na wykresie nawigacji, mają inną instancję ViewModel. W tym przypadku ViewModel jest ograniczony do cyklu życia miejsca docelowego i jest czyszczony, gdy miejsce docelowe zostanie usunięte ze stosu wstecznego. W poniższym przykładzie, gdy użytkownik przechodzi do ekranu Profil, tworzona jest nowa instancja GreetingViewModel.

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

Źródło wiarygodnych danych o stanie

Gdy wdrożysz Compose w jednej części interfejsu, może się okazać, że kod Compose i systemu widoków musi udostępniać dane. Jeśli to możliwe, zalecamy hermetyzowanie tego stanu współdzielonego w innej klasie, która jest zgodna ze sprawdzonymi metodami UDF używanymi przez obie platformy, np. w ViewModel, która udostępnia strumień danych współdzielonych do emitowania aktualizacji danych.

Nie zawsze jest to jednak możliwe, jeśli dane do udostępnienia są zmienne lub ściśle powiązane z elementem interfejsu. W takim przypadku jeden system musi być źródłem wiarygodnych danych i musi udostępniać wszelkie aktualizacje danych drugiemu systemowi. Z reguły źródło wiarygodnych danych powinno należeć do elementu, który jest bliżej korzenia hierarchii interfejsu.

Compose jako źródło wiarygodnych danych

Użyj elementu kompozycyjnego SideEffect , aby opublikować stan Compose w kodzie innym niż Compose. W tym przypadku źródło wiarygodnych danych jest przechowywane w elemencie kompozycyjnym, który wysyła aktualizacje stanu.

Na przykład biblioteka Analytics może umożliwiać segmentowanie użytkowników przez dołączanie niestandardowych metadanych (w tym przykładzie właściwości użytkownika) do wszystkich kolejnych zdarzeń Analytics. Aby przekazać typ użytkownika bieżącego użytkownika do biblioteki Analytics, użyj 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 widoków jako źródło wiarygodnych danych

Jeśli system widoków jest właścicielem stanu i udostępnia go Compose, zalecamy opakowanie stanu w obiekty mutableStateOf, aby był bezpieczny dla Compose. Jeśli używasz tego podejścia, funkcje kompozycyjne są uproszczone, ponieważ nie mają już źródła wiarygodnych danych, ale system widoków musi aktualizować stan zmienny i widoki, które go używają.

W poniższym przykładzie CustomViewGroup zawiera TextView i ComposeView z elementem kompozycyjnym TextField w środku. TextView musi wyświetlać treść wpisywaną przez użytkownika w 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 udostępnionego interfejsu

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

W Compose udostępnione elementy interfejsu stają się elementami kompozycyjnymi, które można ponownie wykorzystać w całej aplikacji, niezależnie od tego, czy element jest stylizowany za pomocą XML, czy jest widokiem niestandardowym. Na przykład utworzysz element kompozycyjny CallToActionButton dla niestandardowego komponentu wezwania do działania Button.

Aby użyć elementu kompozycyjnego na ekranach opartych na widokach, utwórz niestandardowy otokę widoku, która rozszerza AbstractComposeView. W zastąpionym elemencie kompozycyjnym Content umieść utworzony element kompozycyjny opakowany w motyw Compose, jak pokazano w poniższym przykładzie:

@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 elementu kompozycyjnego stają się zmiennymi w widoku niestandardowym. Dzięki temu widok niestandardowy CallToActionViewButton można rozszerzać i używać jak tradycyjny widok. Poniżej znajdziesz przykład tego z użyciem 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 zawiera stan zmienny, zapoznaj się z sekcją Źródło wiarygodnych danych o stanie.

Priorytetowe oddzielanie stanu od prezentacji

Tradycyjnie View jest stanowy. View zarządza polami, które opisują co wyświetlić, a także jak to zrobić. Gdy konwertujesz View na Compose, staraj się oddzielić renderowane dane, aby uzyskać jednokierunkowy przepływ danych, jak opisano w sekcji podnoszenie stanu.

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

Z kolei Compose ułatwia wyświetlanie zupełnie innych elementów kompozycyjnych za pomocą logiki warunkowej w Kotlinie:

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

CautionIcon nie musi wiedzieć ani się przejmować, dlaczego jest wyświetlany, i nie ma pojęcia visibility: albo jest w kompozycji, albo nie.

Dzięki wyraźnemu oddzieleniu zarządzania stanem i logiki prezentacji możesz swobodniej zmieniać sposób wyświetlania treści jako konwersji stanu na interfejs. Możliwość podnoszenia stanu w razie potrzeby sprawia też, że elementy kompozycyjne są bardziej wielokrotnego użytku, ponieważ własność stanu jest bardziej elastyczna.

Promowanie hermetyzowanych komponentów wielokrotnego użytku

Elementy View często mają pewne pojęcie o tym, gdzie się znajdują: w Activity, Dialog, Fragment lub gdzieś w hierarchii innego View. Ponieważ są one często rozszerzane ze statycznych plików układu, ogólna struktura View jest zwykle bardzo sztywna. Powoduje to ściślejsze powiązanie i utrudnia zmianę lub ponowne użycie View.

Na przykład niestandardowy View może zakładać, że ma widok podrzędny określonego typu z określonym identyfikatorem, i bezpośrednio zmieniać jego właściwości w odpowiedzi na jakąś akcję. Powoduje to ścisłe powiązanie tych elementów View: niestandardowy View może się zawiesić lub ulec uszkodzeniu, jeśli nie znajdzie elementu podrzędnego, a element podrzędny prawdopodobnie nie będzie mógł być ponownie użyty bez niestandardowego elementu nadrzędnego View.

W Compose z elementami kompozycyjnymi wielokrotnego użytku jest to mniejszy problem. Elementy nadrzędne mogą łatwo określać stan i wywołania zwrotne, dzięki czemu możesz pisać elementy kompozycyjne wielokrotnego użytku bez konieczności poznania 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 powyższym przykładzie wszystkie 3 części są bardziej hermetyzowane i mniej powiązane:

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

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

  • Dla elementu nadrzędnego nie ma znaczenia, jak głęboko zagnieżdżone są ImageWithEnabledOverlay lub ControlPanelWithToggle. Te elementy podrzędne mogą animować zmiany, zamieniać treści lub przekazywać treści do innych elementów podrzędnych.

Ten wzorzec jest znany jako odwrócenie sterowania. Więcej informacji znajdziesz w dokumentacji CompositionLocal.

Obsługa zmian rozmiaru ekranu

Używanie różnych zasobów dla różnych rozmiarów okien to jeden z głównych sposobów tworzenia elastycznych układów View. Chociaż kwalifikowane zasoby nadal są opcją w przypadku decyzji dotyczących układu na poziomie ekranu, Compose znacznie ułatwia całkowitą zmianę układów w kodzie za pomocą normalnej logiki warunkowej. Więcej informacji znajdziesz w artykule Używanie klas rozmiaru okna.

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

Zagnieżdżone przewijanie z widokami

Więcej informacji o tym, jak włączyć interoperacyjność zagnieżdżonego przewijania między elementami View z możliwością przewijania a elementami kompozycyjnymi z możliwością przewijania, zagnieżdżonymi w obu kierunkach, znajdziesz w artykule Interoperacyjność zagnieżdżonego przewijania.

Compose w RecyclerView

Elementy kompozycyjne w RecyclerView są wydajne od wersji RecyclerView 1.3.0-alpha02. Aby skorzystać z tych zalet, upewnij się, że używasz co najmniej wersji 1.3.0-alpha02 RecyclerView.

Interoperacyjność WindowInsets z widokami

Może być konieczne zastąpienie domyślnych wcięć, gdy ekran zawiera zarówno widoki, jak i kod Compose w tej samej hierarchii. W takim przypadku musisz wyraźnie określić, który element ma zużywać wcięcia, a który ma je ignorować.

Jeśli na przykład Twój najbardziej zewnętrzny układ jest układem widoku Androida, zużywaj wcięcia w systemie widoków i ignoruj je w Compose. Jeśli natomiast Twój najbardziej zewnętrzny układ jest elementem kompozycyjnym, zużywaj wcięcia w Compose i odpowiednio wypełnij elementy kompozycyjne AndroidView.

Domyślnie każdy ComposeView zużywa wszystkie wcięcia na poziomie zużycia WindowInsetsCompat. Aby zmienić to domyślne zachowanie, ustaw ComposeView.consumeWindowInsets na false.

Więcej informacji znajdziesz w dokumentacji WindowInsets w Compose.