Stosowanie sprawdzonych metod

W trakcie tworzenia nowej wiadomości możesz napotkać częste problemy. Błędy te mogą sprawić, że kod będzie działać dobrze, ale może negatywnie wpłynąć na wydajność UI. Postępuj zgodnie ze sprawdzonymi metodami, aby zoptymalizować aplikację pod kątem tworzenia wiadomości.

Użyj metody remember, aby zminimalizować kosztowne obliczenia

Funkcje kompozycyjne mogą być uruchamiane bardzo często, tak często jak w przypadku każdej klatki animacji. Z tego względu zalecamy, aby przeprowadzać jak najmniej obliczeń w treści kompozycyjnej.

Ważną metodą jest przechowywanie wyników obliczeń w remember. Dzięki temu obliczenia będą wykonywane raz, a wyniki możesz pobrać, gdy będą potrzebne.

Na przykład ten kod wyświetla posortowaną listę nazw, ale sortowanie następuje w bardzo kosztowny sposób:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}

Za każdym razem, gdy ContactsList jest tworzony ponownie, cała lista kontaktów jest od nowa sortowana, nawet jeśli nie uległa zmianie. Jeśli użytkownik przewinie listę, element kompozycyjny zostanie utworzony ponownie za każdym razem, gdy pojawi się nowy wiersz.

Aby rozwiązać ten problem, posortuj listę poza polem LazyColumn i zapisz posortowaną listę w remember:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, comparator) {
        contacts.sortedWith(comparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
            // ...
        }
    }
}

Teraz lista jest sortowana raz, gdy tworzony jest element ContactList. Jeśli zmienią się kontakty lub komparator, posortowana lista jest generowana ponownie. W przeciwnym razie funkcja kompozycyjna może nadal korzystać z posortowanej listy zapisanych w pamięci podręcznej.

Używanie klawiszy leniwego układu

Leniwe układy skutecznie ponownie wykorzystują elementy, ale w razie potrzeby generują je ponownie lub tworzą ponownie. Możesz jednak pomóc zoptymalizować leniwe układy pod kątem zmiany kompozycji.

Załóżmy, że operacja użytkownika powoduje przeniesienie elementu na liście. Załóżmy na przykład, że masz listę notatek posortowaną według czasu modyfikacji, gdzie u góry znajduje się ostatnio zmodyfikowana notatka.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

Z kodem jest jednak pewien problem. Załóżmy, że dolna nuta została zmieniona. Jest to ostatnio zmodyfikowana notatka, więc trafia na początek listy. Każda inna notatka przesuwa się w dół o jedno miejsce.

Bez Twojej pomocy funkcja tworzenia wiadomości nie zdaje sobie sprawy, że niezmienione elementy są tylko przenoszone na liście. Zespół Compose uważa, że stary „element 2” został usunięty, a nowy został utworzony dla elementu 3, nr 4 aż do samego końca. W efekcie funkcja Utwórz ponownie tworzy każdy element na liście, choć tylko jeden w rzeczywistości się zmienił.

Rozwiązaniem jest udostępnienie kluczy produktu. Zapewnienie stabilnego klucza dla każdego elementu pozwala w trybie tworzenia uniknąć niepotrzebnych zmian kompozycji. W tym przypadku funkcja tworzenia może określić, że element znajdujący się obecnie w miejscu 3 jest tym samym, który znajdował się na pozycji 2. Nie zmieniły się żadne dane tego elementu, więc w komponencie nie trzeba go ponownie tworzyć.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
            key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

Aby ograniczyć zmiany kompozycji, użyj derivedStateOf

Jednym z ryzyków używania stanu w kompozycjach jest to, że w przypadku gwałtownej zmiany stanu interfejs może zostać przetworzony bardziej, niż jest to konieczne. Załóżmy na przykład, że jest wyświetlana lista przewijana. Przyjrzysz się jej stanowi, by sprawdzić, który element jest na niej pierwszy widoczny:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Problem polega na tym, że jeśli użytkownik przewinie listę, listState stale się zmienia, gdy użytkownik przeciąga palcem. Oznacza to, że lista jest ciągle tworzona. Jednak w rzeczywistości nie musisz często tworzyć nowych elementów – nie musisz ich tworzyć ponownie, dopóki nowy element nie pojawi się na dole. To sporo dodatkowych obliczeń, przez co interfejs działa słabo.

Rozwiązaniem jest użycie stanu pochodnego. Stan derywowany pozwala określić, które zmiany stanu rzeczywiście mają aktywować zmianę kompozycji. W tym przypadku określ, że zależy Ci na tym, kiedy zmieni się pierwszy widoczny element. Gdy wartość to zmieni się, interfejs trzeba będzie utworzyć ponownie, ale jeśli użytkownik nie przewinie jeszcze wystarczająco dużo, by przenieść nowy element na górę, nie będzie musiał ponownie go utworzyć.

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Odłóż odczyty tak najdłużej, jak to możliwe

Gdy wykryjemy problem z wydajnością, pomocne może okazać się odroczenie odczytu stanu. Opóźnienie odczytu stanu sprawi, że funkcja Compose ponownie uruchomi minimalny możliwy kod przy rekompozycji. Jeśli np. interfejs użytkownika zawiera stan, który został podniesiony wysoko w drzewie kompozycyjnym, i odczytujesz stan w podrzędnym elemencie kompozycyjnym, możesz zapakować stan odczytany w funkcję lambda. Dzięki temu odczyt następuje tylko wtedy, gdy faktycznie jest potrzebny. Zobacz implementację w przykładowej aplikacji Jetsnack. Jetsnack implementuje na ekranie szczegółów efekt zwijania się paska narzędzi. Aby dowiedzieć się, dlaczego ta metoda działa, przeczytaj post na blogu Jetpack Compose: Debugging Recomposition.

Aby osiągnąć ten efekt, funkcja kompozycyjna Title wymaga przesunięcia przewijania w celu przesunięcia samej siebie za pomocą funkcji Modifier. Oto uproszczona wersja kodu Jeetsnack przed wykonaniem optymalizacji:

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value)
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }

    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Po zmianie stanu przewijania funkcja tworzenia unieważnia najbliższy zakres zmiany elementu nadrzędnego. W tym przypadku najbliższym zakresem jest funkcja kompozycyjna SnackDetail. Pamiętaj, że Box to funkcja wbudowana, więc nie można jej zmienić. Funkcja Utwórz więc tworzy kompozycję SnackDetail i wszystkie elementy kompozycyjne wewnątrz SnackDetail. Jeśli zmienisz kod tak, aby odczytywał tylko ten stan, w którym faktycznie go używasz, zmniejszysz liczbę elementów do ponownego utworzenia.

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value }
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Parametr przewijania ma teraz postać lambda. Oznacza to, że Title nadal może odwoływać się do stanu podniesienia, ale wartość jest odczytywana tylko w elemencie Title, gdzie jest potrzebna. W rezultacie gdy wartość przewijania zmieni się, najbliższym zakresem zmiany kompozycji jest element Title kompozycyjny – nie trzeba już ponownie tworzyć całego elementu Box.

Jest to dobry wynik, ale możesz zrobić jeszcze lepiej! Jeśli sprawiasz, że kompozycja wpływa tylko na zmianę układu lub ponowne rysowanie elementu kompozycyjnego, zachowaj podejrzenia. W tym przypadku wystarczy zmienić przesunięcie elementu kompozycyjnego Title. Można to zrobić na etapie układu.

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    Column(
        modifier = Modifier
            .offset { IntOffset(x = 0, y = scrollProvider()) }
    ) {
        // ...
    }
}

Wcześniej w kodzie używany był parametr Modifier.offset(x: Dp, y: Dp), który usuwa tę wartość jako parametr. Przełączając się na wersję lambda modyfikatora, możesz mieć pewność, że funkcja będzie odczytywać stan przewijania na etapie układu. Dlatego po zmianie stanu przewijania funkcja tworzenia może całkowicie pominąć etap kompozycji i przejść bezpośrednio do fazy układu. Gdy często zmieniasz zmienne stanu na modyfikatory, używaj ich wersji lambda, gdy tylko to możliwe.

Oto kolejny przykład takiego podejścia. Ten kod nie został jeszcze zoptymalizowany:

// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

W tym przypadku kolor tła pola szybko przełącza się między dwoma kolorami. Stan ten zmienia się bardzo często. Funkcja kompozycyjna odczytuje ten stan w modyfikatorze tła. Z tego powodu pole musi tworzyć nowe kompozycje w każdej klatce, ponieważ kolor zmienia się w każdej klatce.

Aby to poprawić, użyj modyfikatora opartego na lambda, w tym przypadku drawBehind. Oznacza to, że stan koloru jest odczytywany tylko podczas fazy rysowania. W rezultacie funkcja tworzenia może całkowicie pominąć fazy kompozycji i układu. Gdy kolor się zmieni, tworzenie przechodzi bezpośrednio do fazy rysowania.

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
    Modifier
        .fillMaxSize()
        .drawBehind {
            drawRect(color)
        }
)

Unikaj zapisywania odwrotnego

W przypadku tworzenia wiadomości zakładamy, że nigdy nie będziesz zapisywać wiadomości, które zostały już przeczytane. Jest to tzw. zapis wsteczny, który może powodować niekończącą się zmianę kompozycji przy każdej klatce.

Przykład tego rodzaju błędu można zobaczyć w poniższym elemencie kompozycyjnym.

@Composable
fun BadComposable() {
    var count by remember { mutableStateOf(0) }

    // Causes recomposition on click
    Button(onClick = { count++ }, Modifier.wrapContentSize()) {
        Text("Recompose")
    }

    Text("$count")
    count++ // Backwards write, writing to state after it has been read</b>
}

Ten kod aktualizuje liczbę na końcu funkcji kompozycyjnej po odczytaniu jej w poprzednim wierszu. Jeśli uruchomisz ten kod, zobaczysz, że po kliknięciu przycisku, co powoduje zmianę kompozycji, licznik gwałtownie rośnie w nieskończoną pętlę, ponieważ funkcja Compose ponownie tworzy ten kompozycję, widzi odczyt stanu, który jest nieaktualny, więc zaplanuje kolejną zmianę.

Możesz całkowicie uniknąć pisania odwrotnego, nie pisząc nigdy w celu przedstawienia w komponencie. Jeśli to możliwe, zawsze zapisuj wywołania w odpowiedzi na zdarzenie i w funkcji lambda, jak w poprzednim przykładzie onClick.

Dodatkowe materiały