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:
- Material Design 1 z biblioteką
AppCompat (np.
Theme.AppCompat.*) - Material Design 2 z
biblioteką MDC-Android (np.
Theme.MaterialComponents.*) - Material Design 3 z użyciem
biblioteki MDC-Android (np.
Theme.Material3.*)
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:
- Migracja z Material 1 do Material 2 w widokach
- Migracja z Material 2 do Material 3 w widokach
- Migracja z Material 2 do Material 3 w 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.
Nawigacja
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:
ImageWithEnabledOverlaymusi tylko wiedzieć, jaki jest bieżący stanisEnabled. Nie musi wiedzieć, że istniejeControlPanelWithToggle, ani nawet jak można go kontrolować.ControlPanelWithTogglenie wie, że istniejeImageWithEnabledOverlay. Może istnieć 0, 1 lub więcej sposobów wyświetlaniaisEnabled, aControlPanelWithTogglenie będzie musiał się zmieniać.Dla elementu nadrzędnego nie ma znaczenia, jak głęboko zagnieżdżone są
ImageWithEnabledOverlaylubControlPanelWithToggle. 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.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Wyświetlanie emoji
- Material Design 2 w Compose
- Wcięcia okna w Compose