Migracja z Widoków do Jetpack Compose dotyczy wyłącznie interfejsu, ale aby przeprowadzić ją bezpiecznie i stopniowo, musisz wziąć pod uwagę wiele kwestii. Na tej stronie znajdziesz kilka kwestii, które warto wziąć pod uwagę podczas migracji 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 użyciem biblioteki MDC-Android (np.
Theme.MaterialComponents.*
). - Material Design 3 z użyciem biblioteki MDC-Android (np.
Theme.Material3.*
)
W przypadku aplikacji napisanych w Compose dostępne są 2 wersje Material Design:
- Material Design 2 z użyciem biblioteki Compose Material (np.
androidx.compose.material.MaterialTheme
) - Material Design 3 z użyciem biblioteki 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 po migracji zarówno w przypadku widoków, jak i Compose:
- Materiał 1 do Materiału 2 w widokach
- Materiał 2 do Materiału 3 w widokach
- Materiał 2 do Materiału 3 w funkcji Tworzenie
Podczas tworzenia nowych ekranów w Compose, niezależnie od używanej wersji Material Design, przed wszystkimi komponentami, które emitują interfejs z bibliotek Compose Material, zastosuj MaterialTheme
. Komponenty Material (Button
, Text
itp.) wymagają obecności elementu MaterialTheme
, a bez niego ich działanie jest nieokreślone.
Wszystkie przykłady Jetpack Compose korzystają z niestandardowego motywu Compose opartego na MaterialTheme
.
Więcej informacji znajdziesz w artykułach Systemy projektowe 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 w Compose – interoperacyjność i Migracja z Jetpack Navigation na Navigation Compose.
Testowanie interfejsu mieszanego Compose/Views
Po przeniesieniu części aplikacji do Compose testowanie jest kluczowe, aby upewnić się, że nic nie zostało uszkodzone.
Gdy aktywność lub fragment używa Compose, musisz użyć createAndroidComposeRule
zamiast ActivityScenarioRule
. createAndroidComposeRule
jest zintegrowanyActivityScenarioRule
z ComposeTestRule
, który umożliwia jednoczesne testowanie kodu Compose i widoku.
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 współdziałaniu z platformami testowania interfejsu znajdziesz w sekcjach Współdziałanie z Espresso i Współdziałanie z UiAutomator.
Integracja Compose z dotychczasową architekturą aplikacji
Wzorce architektury jednokierunkowego przepływu danych (UDF) działają bezproblemowo z Compose. Jeśli aplikacja korzysta z innych wzorców architektury, np. Model-View-Presenter (MVP), zalecamy przeniesienie tej części interfejsu do UDF przed wdrożeniem Compose lub w jego trakcie.
Używanie ViewModel
w komponowaniu wiadomości
Jeśli używasz biblioteki Architecture ComponentsViewModel
, możesz uzyskać dostęp do ViewModel
z dowolnego komponentu kompozycyjnego, wywołując funkcję viewModel()
, jak wyjaśniono w sekcji Compose i inne biblioteki.
Podczas wdrażania Compose zachowaj ostrożność przy używaniu tego samego typu ViewModel
w różnych komponentach, ponieważ elementy ViewModel
są zgodne z zakresami cyklu życia widoku. Zakres będzie obejmować aktywność hosta, fragment lub wykres nawigacji, jeśli używana jest biblioteka Navigation.
Jeśli np. funkcje kompozycyjne są hostowane w aktywności, funkcja viewModel()
zawsze zwraca tę samą instancję, która jest czyszczona dopiero po zakończeniu aktywności.
W poniższym przykładzie ten sam użytkownik („user1”) jest witany 2 razy, ponieważ ten sam obiekt GreetingViewModel
jest ponownie używany we wszystkich komponentach kompozycyjnych w ramach aktywności hosta. Pierwsza utworzona instancja ViewModel
jest ponownie używana w innych 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 } }
W przypadku wykresów nawigacji elementy ViewModel
również mają określony zakres, więc funkcje 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 usuwany, gdy miejsce docelowe zostanie usunięte z listy wstecznej. W poniższym przykładzie, gdy użytkownik przejdzie do ekranu Profil, zostanie utworzona nowa instancja GreetingViewModel
.
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
Źródło danych o stanie
Gdy w jednej części interfejsu użytkownika zastosujesz Compose, może się okazać, że kod Compose i systemu widoków musi współdzielić dane. Jeśli to możliwe, zalecamy umieszczenie 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 informacji, a wszelkie aktualizacje danych muszą być udostępniane drugiemu systemowi. Z zasady źródło informacji powinno należeć do elementu, który znajduje się bliżej korzenia hierarchii interfejsu.
Tworzenie jako źródło wiarygodnych informacji
Użyj funkcji
SideEffect
composable, aby opublikować stan Compose w kodzie innym niż Compose. W tym przypadku źródło prawdy jest przechowywane w funkcji kompozycyjnej, która wysyła aktualizacje stanu.
Na przykład biblioteka analityczna może umożliwiać segmentowanie użytkowników przez dołączanie do wszystkich kolejnych zdarzeń analitycznych niestandardowych metadanych (w tym przykładzie właściwości użytkownika). Aby przekazać do biblioteki analitycznej typ użytkownika, który obecnie korzysta z aplikacji, użyj funkcji 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.
Traktuj system jako źródło wiarygodnych informacji
Jeśli stan jest własnością systemu widoków i jest udostępniany Compose, zalecamy opakowanie stanu w obiekty mutableStateOf
, aby był bezpieczny dla Compose w wielowątkowym środowisku. Jeśli użyjesz tego podejścia, funkcje kompozycyjne zostaną uproszczone, ponieważ nie będą już zawierać źródła prawdy, ale system widoków będzie musiał aktualizować stan modyfikowalny i widoki, które z niego korzystają.
W tym przykładzie CustomViewGroup
zawiera TextView
i ComposeView
z elementem TextField
, który można komponować. TextView
musi wyświetlać treść wpisaną 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 przechodzisz na Compose, możesz potrzebować wspólnych elementów interfejsu w Compose i systemie widoków. Jeśli na przykład Twoja aplikacja ma niestandardowy komponent CallToActionButton
, możesz go używać zarówno na ekranach opartych na Compose, jak i na ekranach opartych na widokach.
W Compose udostępnione elementy interfejsu stają się funkcjami kompozycyjnymi, których można używać ponownie w całej aplikacji, niezależnie od tego, czy element jest stylizowany za pomocą XML, czy jest widokiem niestandardowym. Na przykład możesz utworzyć CallToActionButton
komponent kompozycyjny dla niestandardowego komponentu Button
wezwania do działania.
Aby użyć komponentu w przypadku ekranów opartych na widokach, utwórz niestandardowy element opakowujący widok, który rozszerza AbstractComposeView
. W zastąpionym elemencie kompozycyjnym Content
umieść utworzony element kompozycyjny opakowany w motyw Compose, 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 kompozycyjne stają się w niestandardowym widoku zmiennymi, które można modyfikować. Dzięki temu widok niestandardowy CallToActionViewButton
jest rozszerzalny i można go używać jak tradycyjnego widoku. Przykład znajdziesz poniżej w sekcji View Binding:
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 modyfikowalny, zapoznaj się z sekcją Źródło stanu.
Nadaj priorytet oddzieleniu stanu od prezentacji
Tradycyjnie View
jest stanowy. View
zarządza polami, które opisują co ma być wyświetlane, a także jak ma to być wyświetlane. Gdy przekształcasz View
w Compose, staraj się rozdzielać renderowane dane, aby uzyskać jednokierunkowy przepływ danych, co zostało dokładniej opisane w sekcji przenoszenie stanu.
Na przykład element View
ma właściwość visibility
, która określa, czy jest widoczny, niewidoczny czy usunięty. Jest to nieodłączna właściwość View
. Chociaż inne fragmenty kodu mogą zmieniać widoczność elementu View
, tylko sam element View
wie, jaka jest jego bieżąca widoczność. Logika zapewniająca widoczność elementu View
może być podatna na błędy i jest często powiązana z samym elementem View
.
Natomiast Compose ułatwia wyświetlanie zupełnie różnych komponentów kompozycyjnych za pomocą logiki warunkowej w Kotlinie:
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
Z założenia CautionIcon
nie musi wiedzieć ani się przejmować tym, dlaczego jest wyświetlany, i nie ma pojęcia o visibility
: albo jest w kompozycji, albo nie.
Dzięki wyraźnemu rozdzieleniu zarządzania stanem i logiki prezentacji możesz swobodniej zmieniać sposób wyświetlania treści jako konwersji stanu na interfejs. Możliwość przenoszenia stanu w górę w razie potrzeby sprawia też, że funkcje kompozycyjne są bardziej wielokrotnego użytku, ponieważ własność stanu jest bardziej elastyczna.
Promowanie hermetyzowanych komponentów do wielokrotnego użytku
Elementy View
często mają pewne pojęcie o tym, gdzie się znajdują: w elemencie Activity
, Dialog
, Fragment
lub w innym miejscu w hierarchii View
. Ponieważ są one często tworzone na podstawie statycznych plików układu, ogólna struktura View
jest zwykle bardzo sztywna. Powoduje to silniejsze 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 o określonym identyfikatorze, i bezpośrednio zmieniać jego właściwości w odpowiedzi na jakąś czynność. Powoduje to ścisłe powiązanie tych elementów View
: niestandardowy element View
może ulec awarii lub uszkodzeniu, jeśli nie może znaleźć elementu podrzędnego, a elementu podrzędnego prawdopodobnie nie można ponownie użyć bez elementu nadrzędnego View
.
W przypadku komponentów wielokrotnego użytku w Compose ten problem jest mniejszy. Rodzice mogą łatwo określać stan i wywołania zwrotne, dzięki czemu możesz pisać komponenty 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 od siebie odseparowane i mniej ze sobą powiązane:
ImageWithEnabledOverlay
musi tylko znać bieżący stanisEnabled
. Nie musi wiedzieć, że istniejeControlPanelWithToggle
, ani nawet tego, jak można nim sterować.ControlPanelWithToggle
nie wie, żeImageWithEnabledOverlay
istnieje.isEnabled
może być wyświetlane na 0, 1 lub więcej sposobów, aControlPanelWithToggle
nie musi się zmieniać.Dla elementu nadrzędnego nie ma znaczenia, jak głęboko zagnieżdżone są elementy
ImageWithEnabledOverlay
lubControlPanelWithToggle
. Mogą one animować zmiany, wymieniać treści lub przekazywać je innym dzieciom.
Ten wzorzec jest znany jako odwrócenie kontroli. Więcej informacji na ten temat znajdziesz w CompositionLocal
dokumentacji.
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 View
układów. Chociaż kwalifikowane zasoby nadal są opcją przy podejmowaniu decyzji dotyczących układu na poziomie ekranu, Compose znacznie ułatwia całkowitą zmianę układów w kodzie za pomocą zwykłej logiki warunkowej. Więcej informacji znajdziesz w artykule Używanie klas rozmiaru okna.
Więcej informacji o technikach, które Compose oferuje do tworzenia adaptacyjnych interfejsów, znajdziesz w artykule Obsługa różnych rozmiarów ekranów.
Zagnieżdżone przewijanie w przypadku widoków
Więcej informacji o włączaniu zagnieżdżonego przewijania między elementami View z możliwością przewijania a komponentami z możliwością przewijania zagnieżdżonymi w obu kierunkach znajdziesz w artykule Zagnieżdżone przewijanie.
Tworzenie w RecyclerView
Funkcje kompozycyjne w RecyclerView
są wydajne od wersji RecyclerView
1.3.0-alpha02. Aby zobaczyć te korzyści, musisz mieć co najmniej wersję 1.3.0-alpha02 biblioteki RecyclerView
.
WindowInsets
zgodność 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 korzystać z wcięć, a który ma je ignorować.
Jeśli np. najbardziej zewnętrzny układ to układ widoku Androida, musisz wykorzystać wstawki w systemie widoku i zignorować je w Compose.
Jeśli najbardziej zewnętrzny układ jest komponentem, możesz użyć wstawek w Compose i odpowiednio dodać do komponentów AndroidView
dopełnienie.
Domyślnie każdy element ComposeView
zużywa wszystkie wstawki na poziomie WindowInsetsCompat
. Aby zmienić to domyślne działanie, ustaw wartość
ComposeView.consumeWindowInsets
na false
.
Więcej informacji znajdziesz w dokumentacji WindowInsets
w Compose.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony.
- Wyświetlanie emotikonów
- Material Design 2 w Compose
- Wstawki okien w Compose