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:
- Widok Material 1–Material 2
- Materiał 2 do Materiału 3 w sekcji Widoki
- Materiał 2 do Materiału 3 w sekcji „Składanie”
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.
Nawigacja
Jeśli w swojej aplikacji używasz komponentu Nawigacja, zapoznaj się z artykułami Nawigacja w Compose – interoperacyjność i 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 Espresso i Interoperencja 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 stanisEnabled
. Nie musi wiedzieć, żeControlPanelWithToggle
istnieje, ani jak można go kontrolować.ControlPanelWithToggle
nie wie, żeImageWithEnabledOverlay
istnieje. Może istnieć zero, jeden lub więcej sposobów wyświetlaniaisEnabled
, aControlPanelWithToggle
nie musi się zmieniać.Dla elementu nadrzędnego nie ma znaczenia, jak głęboko
ImageWithEnabledOverlay
lubControlPanelWithToggle
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.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Wyświetlanie emotikonów
- Material Design 2 w Compose
- Wstawki okna w sekcji Tworzenie