Chociaż przejście z Widoków na Compose jest związane wyłącznie z interfejsem użytkownika, należy wziąć pod uwagę wiele kwestii, aby przeprowadzić bezpieczną i stopniową migrację. Ta strona zawiera informacje, o których należy pamiętać podczas przenoszenia aplikacji opartej na View 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 wykorzystaniem biblioteki AppCompat (np.
Theme.AppCompat.*
) - Material Design 2 z użyciem 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 interfejsu 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 po migracji dotyczące zarówno widoków, jak i komponowania:
- Materiał 1 do Materiału 2 w sekcji Widoki
- 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, upewnij się, że przed każdym komponentem, który emituje interfejs z bibliotek Material Compose, stosujesz MaterialTheme
. Składniki interfejsu Material (Button
, Text
itp.) zależą od tego, czy MaterialTheme
jest aktywna. Bez niej ich działanie jest nieokreślone.
Wszystkie próbki Jetpack Compose korzystają z niestandardowego motywu Compose utworzonego na podstawie MaterialTheme
.
Więcej informacji znajdziesz w artykułach Systemy projektowania w Compose i Przenoszenie 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 aktywność lub fragment używa funkcji Compose, zamiast ActivityScenarioRule
musisz użyć createAndroidComposeRule
. 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() } }
Aby dowiedzieć się więcej o testowaniu, przeczytaj artykuł Testowanie układu okna 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 dopasowuje się do Compose. Jeśli aplikacja używa innych wzorów architektury, np. Model View Presenter (MVP), zalecamy przeniesienie tej części interfejsu użytkownika do UDF przed wdrożeniem Compose lub w trakcie jego wdrażania.
Korzystanie z ViewModel
w sekcji Tworzenie
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.
Podczas korzystania z kompozycji należy uważać, aby w różnych składanych elementach używać tego samego typu ViewModel
, ponieważ elementy ViewModel
są zgodne z zakresem 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 tym przykładzie ten sam użytkownik („user1”) jest witany dwukrotnie, ponieważ ta sama instancja GreetingViewModel
jest używana ponownie we wszystkich składaniach w ramach hostowanej aktywności. 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ą celem w grafie nawigacyjnym, mają inną instancję elementu ViewModel
.
W tym przypadku ViewModel
jest ograniczone do cyklu życia miejsca docelowego i jest usuwane, gdy miejsce docelowe zostanie usunięte z backstacka. W tym przykładzie, gdy użytkownik przechodzi na ekran Profil, tworzony jest nowy element 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. Zalecamy, aby w miarę możliwości otaczać ten stan w innej klasie, która stosuje się do sprawdzonych metod UDF używanych na obu platformach. Możesz to zrobić na przykład w klasie ViewModel
, która udostępnia strumień współdzielonych danych, aby emitować aktualizacje danych.
Nie zawsze jest to jednak możliwe, jeśli dane, które mają być udostępniane, są zmienne lub ściśle powiązane z elementem interfejsu użytkownika. W takim przypadku jeden system musi być źródłem prawdy i musi udostępniać wszelkie aktualizacje danych do drugiego systemu. 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
Użyj komponentu SideEffect
, aby opublikować stan Compose w kodzie niebędącym kodem Compose. 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 zmiennej SideEffect
do zaktualizowania jej wartości.
@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 stan jest obsługiwany przez system View i jest udostępniany komponentowi Compose, zalecamy owinięcie stanu w obiekty mutableStateOf
, aby zapewnić bezpieczeństwo wątku dla Compose. Jeśli zastosujesz to podejście, funkcje kompozytowe zostaną uproszczone, ponieważ nie będą już mieć źródła prawdy, ale system View musi zaktualizować stan zmienny i widoki, które go używają.
W tym przykładzie element CustomViewGroup
zawiera element TextView
i element ComposeView
z elementem kompozytowym TextField
. 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
Jeśli stopniowo przechodzisz na Compose, możesz potrzebować elementów interfejsu użytkownika w Compose i systemie 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 wspólne elementy interfejsu użytkownika stają się komponentami, których można używać w całej aplikacji niezależnie od tego, czy element ma styl za pomocą kodu XML, czy jest widokiem niestandardowym. Na przykład możesz utworzyć komponent CallToActionButton
dla niestandardowego wezwania do działania Button
.
Aby używać komponentu na ekranach opartych na widoku, utwórz niestandardowy element opakowujący widok, który rozszerza się z poziomu AbstractComposeView
. W elementzie kompozycyjnym Content
z przesłoniętym tematem 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 w widoku niestandardowym. 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 stan, który można zmienić, zapoznaj się z artykułem Źródło stanu.
Priorytetowe traktowanie stanu 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 prezentacji 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.
Promowanie opakowanych i wielorazowych komponentów
Elementy View
często mają pewną wiedzę o tym, gdzie się znajdują: w elementach Activity
, Dialog
, Fragment
lub gdzieś w hierarchii innego elementu View
. Ponieważ są one często tworzone na podstawie statycznych plików układu, ogólna struktura pliku View
jest zwykle 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 jakąś czynność. 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 kompozytorów z wielokrotnego użytku problem ten jest mniej dotkliwy. Rodzice mogą łatwo określać stan i wywołania zwrotne, dzięki czemu możesz pisać wielokrotnie użyte komponenty bez konieczności znajomości dokładnego miejsca ich użycia.
@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 są zagnieżdżone elementy
ImageWithEnabledOverlay
lubControlPanelWithToggle
. Dzieci mogą animować zmiany, zamieniać treści lub przekazywać je innym dzieciom.
Ten wzór nosi nazwę inwersja kontroli. Więcej informacji znajdziesz w dokumentacjiCompositionLocal
.
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. Aby dowiedzieć się więcej, zapoznaj się z artykułem Używanie klas rozmiarów okien.
Dodatkowo zapoznaj się z artykułem Obsługa różnych rozmiarów ekranów, aby dowiedzieć się więcej o technikach, które Compose oferuje do tworzenia interfejsów adaptacyjnych.
Zagnieżdżone przewijanie za pomocą widoków
Więcej informacji o włączaniu obsługi sterowania przewijaniem w głębokości między elementami widoku z możliwością przewijania i komponowanymi elementami z możliwością przewijania, które są ułożone w głębokości w obu kierunkach, znajdziesz w artykule Interoperacyjność elementów widoku z możliwością przewijania i komponowanych elementów z możliwością przewijania.
Napisz w RecyclerView
Komponenty w RecyclerView
są wydajne od wersji RecyclerView
1.3.0-alpha02. 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 najszerszy układ jest układem Android View, powinieneś używać wstawek w systemie View i ignorować je w Compose.
Jeśli natomiast zewnętrzny układ jest składanym elementem, musisz użyć w Compose wbudowanych elementów i odpowiednio uzupełnić składane elementy AndroidView
.
Domyślnie każda 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
- Okna w edytorze